Skip to content
3 changes: 2 additions & 1 deletion packages/next/src/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AuthCollectionSlug, LoginResult, MaybePromise, SanitizedConfig } f

import { getPayload } from 'payload'

import { nextAppCronGetPayloadOptions } from '../utilities/nextAppCronGetPayloadOptions.js'
import { setPayloadAuthCookie } from '../utilities/setPayloadAuthCookie.js'

type LoginWithEmail<TSlug extends AuthCollectionSlug> = {
Expand All @@ -30,7 +31,7 @@ export async function login<TSlug extends AuthCollectionSlug>({
password,
username,
}: LoginArgs<TSlug>): Promise<LoginResult<TSlug>> {
const payload = await getPayload({ config, cron: true })
const payload = await getPayload({ config, ...nextAppCronGetPayloadOptions })

const authConfig = payload.collections[collection]?.config.auth

Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cookies as getCookies, headers as nextHeaders } from 'next/headers.js'
import { createLocalReq, getPayload, logoutOperation } from 'payload'

import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'
import { nextAppCronGetPayloadOptions } from '../utilities/nextAppCronGetPayloadOptions.js'

export async function logout({
allSessions = false,
Expand All @@ -14,7 +15,7 @@ export async function logout({
allSessions?: boolean
config: MaybePromise<SanitizedConfig>
}) {
const payload = await getPayload({ config, cron: true })
const payload = await getPayload({ config, ...nextAppCronGetPayloadOptions })
const headers = await nextHeaders()
const authResult = await payload.auth({ headers })

Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/auth/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { headers as nextHeaders } from 'next/headers.js'
import { createLocalReq, getPayload, refreshOperation } from 'payload'

import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'
import { nextAppCronGetPayloadOptions } from '../utilities/nextAppCronGetPayloadOptions.js'
import { setPayloadAuthCookie } from '../utilities/setPayloadAuthCookie.js'

export async function refresh({ config }: { config: MaybePromise<SanitizedConfig> }) {
const payload = await getPayload({ config, cron: true })
const payload = await getPayload({ config, ...nextAppCronGetPayloadOptions })
const headers = await nextHeaders()
const result = await payload.auth({ headers })

Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/utilities/initReq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from 'payload'

import { getRequestLocale } from './getRequestLocale.js'
import { nextAppCronGetPayloadOptions } from './nextAppCronGetPayloadOptions.js'
import { selectiveCache } from './selectiveCache.js'

type PartialResult = {
Expand Down Expand Up @@ -46,7 +47,7 @@ export const initReq = async function ({

const partialResult = await partialReqCache.get(async () => {
const config = await configPromise
const payload = await getPayload({ config, cron: true, importMap })
const payload = await getPayload({ config, importMap, ...nextAppCronGetPayloadOptions })
const languageCode = getRequestLanguage({
config,
cookies,
Expand Down
11 changes: 11 additions & 0 deletions packages/next/src/utilities/nextAppCronGetPayloadOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { InitOptions } from 'payload'

import { wrapPayloadJobsRunnerForNext } from './wrapPayloadJobsRunnerForNext.js'

export const nextAppCronGetPayloadOptions: Pick<
InitOptions,
'cron' | 'wrapJobsRunnerInAsyncContext'
> = {
cron: true,
wrapJobsRunnerInAsyncContext: wrapPayloadJobsRunnerForNext,
}
9 changes: 9 additions & 0 deletions packages/next/src/utilities/wrapPayloadJobsRunnerForNext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { workUnitAsyncStorage } from 'next/dist/server/app-render/work-unit-async-storage.external.js'

/**
* Clears Next.js work unit storage for this async scope so job handlers can call
* `revalidatePath` / `revalidateTag` without being attributed to the App Router render phase.
*/
export function wrapPayloadJobsRunnerForNext(run: () => Promise<void>): Promise<void> {
return workUnitAsyncStorage.run(undefined as never, run)
}
7 changes: 7 additions & 0 deletions packages/payload/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,13 @@ export type InitOptions = {
* A function that is called immediately following startup that receives the Payload instance as it's only argument.
*/
onInit?: (payload: Payload) => Promise<void> | void

/**
* Runs autorun job ticks inside a detached async context. Required on Next.js App Router so
* `revalidatePath` / `revalidateTag` inside task handlers are not treated as running during RSC render
* (see Next.js work unit async storage).
*/
wrapJobsRunnerInAsyncContext?: (run: () => Promise<void>) => Promise<void>
}

/**
Expand Down
65 changes: 39 additions & 26 deletions packages/payload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,11 +489,11 @@ export class BasePayload {

email!: InitializedEmailAdapter

encrypt = encrypt

// TODO: re-implement or remove?
// errorHandler: ErrorHandler

encrypt = encrypt

extensions!: (args: {
args: OperationArgs<any>
req: graphQLRequest<unknown, unknown>
Expand Down Expand Up @@ -709,6 +709,8 @@ export class BasePayload {
[slug: string]: any // TODO: Type this
} = {}

wrapJobsRunnerInAsyncContext?: InitOptions['wrapJobsRunnerInAsyncContext']

async _initializeCrons() {
if (this.config.jobs.enabled && this.config.jobs.autoRun && !isNextBuild()) {
const DEFAULT_CRON = '* * * * *'
Expand All @@ -724,36 +726,42 @@ export class BasePayload {
const jobAutorunCron = new Cron(
cronConfig.cron ?? DEFAULT_CRON,
async () => {
if (
_internal_jobSystemGlobals.shouldAutoSchedule &&
!cronConfig.disableScheduling &&
this.config.jobs.scheduling
) {
await this.jobs.handleSchedules({
allQueues: cronConfig.allQueues,
queue: cronConfig.queue,
})
}
const runTick = async () => {
if (
_internal_jobSystemGlobals.shouldAutoSchedule &&
!cronConfig.disableScheduling &&
this.config.jobs.scheduling
) {
await this.jobs.handleSchedules({
allQueues: cronConfig.allQueues,
queue: cronConfig.queue,
})
}

if (!_internal_jobSystemGlobals.shouldAutoRun) {
return
}
if (!_internal_jobSystemGlobals.shouldAutoRun) {
return
}

if (typeof this.config.jobs.shouldAutoRun === 'function') {
const shouldAutoRun = await this.config.jobs.shouldAutoRun(this)
if (typeof this.config.jobs.shouldAutoRun === 'function') {
const shouldAutoRun = await this.config.jobs.shouldAutoRun(this)

if (!shouldAutoRun) {
jobAutorunCron.stop()
return
if (!shouldAutoRun) {
jobAutorunCron.stop()
return
}
}

await this.jobs.run({
allQueues: cronConfig.allQueues,
limit: cronConfig.limit ?? DEFAULT_LIMIT,
queue: cronConfig.queue,
silent: cronConfig.silent,
})
}

await this.jobs.run({
allQueues: cronConfig.allQueues,
limit: cronConfig.limit ?? DEFAULT_LIMIT,
queue: cronConfig.queue,
silent: cronConfig.silent,
})
const wrap = this.wrapJobsRunnerInAsyncContext ?? ((fn: () => Promise<void>) => fn())

await wrap(runTick)
},
{
catch: (err) => {
Expand Down Expand Up @@ -837,6 +845,7 @@ export class BasePayload {
}

this.config = await options.config
this.wrapJobsRunnerInAsyncContext = options.wrapJobsRunnerInAsyncContext
this.logger = getLogger('payload', this.config.logger)

if (!this.config.secret) {
Expand Down Expand Up @@ -1171,6 +1180,10 @@ export const getPayload = async (
}

if (cached.payload) {
if (options.wrapJobsRunnerInAsyncContext) {
cached.payload.wrapJobsRunnerInAsyncContext = options.wrapJobsRunnerInAsyncContext
}

if (options.cron && !cached.initializedCrons) {
// getPayload called with crons enabled, but existing cached version does not have crons initialized. => Initialize crons in existing cached version
cached.initializedCrons = true
Expand Down
Loading