diff --git a/packages/payload-cloud/package.json b/packages/payload-cloud/package.json index 4a35680edc0..160e9999398 100644 --- a/packages/payload-cloud/package.json +++ b/packages/payload-cloud/package.json @@ -57,6 +57,7 @@ "ts-jest": "^29.1.0" }, "peerDependencies": { + "croner": "9.0.0", "payload": "workspace:*" }, "publishConfig": { diff --git a/packages/payload-cloud/src/plugin.spec.ts b/packages/payload-cloud/src/plugin.spec.ts index 3deeddad772..003b2804d77 100644 --- a/packages/payload-cloud/src/plugin.spec.ts +++ b/packages/payload-cloud/src/plugin.spec.ts @@ -165,6 +165,38 @@ describe('plugin', () => { }) }) }) + + describe('cron jobs', () => { + test('should always set global instance identifier, even with no cron jobs or enabled: false', async () => { + const plugin = payloadCloudPlugin({ + cronJobs: { + enabled: false, + }, + }) + const config = await plugin(createConfig()) + + const globalInstance = config.globals?.find( + (global) => global.slug === 'payload-cloud-instance', + ) + + expect(globalInstance).toBeDefined() + expect(globalInstance?.fields).toStrictEqual([ + { + name: 'instance', + type: 'text', + required: true, + }, + ]), + expect(globalInstance?.admin?.hidden).toStrictEqual(true) + + const plugin2 = payloadCloudPlugin() + const config2 = await plugin(createConfig()) + const globalInstance2 = config2.globals?.find( + (global) => global.slug === 'payload-cloud-instance', + ) + expect(globalInstance2).toBeDefined() + }) + }) }) function assertCloudStorage(config: Config) { diff --git a/packages/payload-cloud/src/plugin.ts b/packages/payload-cloud/src/plugin.ts index f0d0e56d3f0..9c1914ab5f0 100644 --- a/packages/payload-cloud/src/plugin.ts +++ b/packages/payload-cloud/src/plugin.ts @@ -1,5 +1,7 @@ import type { Config } from 'payload' +import { Cron } from 'croner' + import type { PluginOptions } from './types.js' import { payloadCloudEmail } from './email.js' @@ -93,5 +95,79 @@ export const payloadCloudPlugin = }) } + // We set up cron jobs on init. + // We also make sure to only run on one instance using a instance identifier stored in a global. + + // If you modify this defaults, make sure to update the TsDoc in the types file. + const DEFAULT_CRON = '* * * * *' + const DEFAULT_LIMIT = 10 + const DEFAULT_CRON_JOB = { + cron: DEFAULT_CRON, + limit: DEFAULT_LIMIT, + queue: 'default (every minute)', + } + config.globals = [ + ...(config.globals || []), + { + slug: 'payload-cloud-instance', + admin: { + hidden: true, + }, + fields: [ + { + name: 'instance', + type: 'text', + required: true, + }, + ], + }, + ] + const newOnInit = async (payload) => { + if (config.onInit) { + await config.onInit(payload) + } + const instance = generateRandomString() + + await payload.updateGlobal({ + slug: 'payload-cloud-instance', + data: { + instance, + }, + }) + + await waitRandomTime() + + const cloudInstance = await payload.findGlobal({ + slug: 'payload-cloud-instance', + }) + + const cronJobs = pluginOptions?.cronJobs?.run ?? [DEFAULT_CRON_JOB] + if (cloudInstance.instance === instance) { + cronJobs.forEach((cronConfig) => { + new Cron(cronConfig.cron ?? DEFAULT_CRON, async () => { + await payload.jobs.run({ + limit: cronConfig.limit ?? DEFAULT_LIMIT, + queue: cronConfig.queue, + }) + }) + }) + } + } + + config.onInit = newOnInit + return config } + +function waitRandomTime(): Promise { + const min = 1000 // 1 second in milliseconds + const max = 5000 // 5 seconds in milliseconds + const randomTime = Math.floor(Math.random() * (max - min + 1)) + min + + return new Promise((resolve) => setTimeout(resolve, randomTime)) +} + +function generateRandomString(): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + return Array.from({ length: 24 }, () => chars[Math.floor(Math.random() * chars.length)]).join('') +} diff --git a/packages/payload-cloud/src/types.ts b/packages/payload-cloud/src/types.ts index 642440fbdce..f1595ebf935 100644 --- a/packages/payload-cloud/src/types.ts +++ b/packages/payload-cloud/src/types.ts @@ -53,7 +53,57 @@ export interface PayloadCloudEmailOptions { skipVerify?: boolean } +export type CronConfig = { + /** + * The cron schedule for the job. + * @default '* * * * *' (every minute). + * + * @example + * ┌───────────── minute (0 - 59) + * │ ┌───────────── hour (0 - 23) + * │ │ ┌───────────── day of the month (1 - 31) + * │ │ │ ┌───────────── month (1 - 12) + * │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday) + * │ │ │ │ │ + * │ │ │ │ │ + * - '0 * * * *' every hour at minute 0 + * - '0 0 * * *' daily at midnight + * - '0 0 * * 0' weekly at midnight on Sundays + * - '0 0 1 * *' monthly at midnight on the 1st day of the month + * - '0/5 * * * *' every 5 minutes + */ + cron?: string + /** + * The limit for the job. This can be overridden by the user. Defaults to 10. + */ + limit?: number + /** + * The queue name for the job. + */ + queue?: string +} + export interface PluginOptions { + /** + * Jobs configuration. By default, there is a single + * cron job that runs every minute. + */ + cronJobs?: { + /** + * Enable the cron jobs defined in the `run` array, + * or the default cron job if `run` is not defined. + * @default true + * @note If you change this in a development environment, + * you will need to restart the server for the changes to take effect. + */ + enabled?: boolean + /** + * Cron jobs configuration. If not defined, a single + * cron job is created that runs every minute. + */ + run?: CronConfig[] + } + /** Payload Cloud Email * @default true */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1459dad9c1d..5076edfe168 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,7 +45,7 @@ importers: version: 1.48.1 '@sentry/nextjs': specifier: ^8.33.1 - version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.3(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13))) + version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13))) '@sentry/node': specifier: ^8.33.1 version: 8.37.1 @@ -147,7 +147,7 @@ importers: version: 9.5.0(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0))) next: specifier: 15.0.3 - version: 15.0.3(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) + version: 15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) open: specifier: ^10.1.0 version: 10.1.0 @@ -946,6 +946,9 @@ importers: amazon-cognito-identity-js: specifier: ^6.1.2 version: 6.3.12 + croner: + specifier: 9.0.0 + version: 9.0.0 nodemailer: specifier: 6.9.10 version: 6.9.10 @@ -1087,7 +1090,7 @@ importers: dependencies: '@sentry/nextjs': specifier: ^8.33.1 - version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.4(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13))) + version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13))) '@sentry/types': specifier: ^8.33.1 version: 8.37.1 @@ -1434,7 +1437,7 @@ importers: version: link:../plugin-cloud-storage uploadthing: specifier: 7.3.0 - version: 7.3.0(next@15.0.4(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)) + version: 7.3.0(next@15.0.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)) devDependencies: payload: specifier: workspace:* @@ -1708,7 +1711,7 @@ importers: version: link:../packages/ui '@sentry/nextjs': specifier: ^8.33.1 - version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.3(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13))) + version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13))) '@sentry/react': specifier: ^7.77.0 version: 7.119.2(react@19.0.0) @@ -1753,7 +1756,7 @@ importers: version: 8.8.3(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3) next: specifier: 15.0.3 - version: 15.0.3(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) + version: 15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) nodemailer: specifier: 6.9.10 version: 6.9.10 @@ -13662,7 +13665,7 @@ snapshots: '@sentry/utils': 7.119.2 localforage: 1.10.0 - '@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.3(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13)))': + '@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13)))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0) @@ -13678,7 +13681,7 @@ snapshots: '@sentry/vercel-edge': 8.37.1 '@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13))) chalk: 3.0.0 - next: 15.0.3(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) + next: 15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) resolve: 1.22.8 rollup: 3.29.5 stacktrace-parser: 0.1.10 @@ -13691,7 +13694,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.4(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13)))': + '@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.0.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13)))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0) @@ -13707,7 +13710,7 @@ snapshots: '@sentry/vercel-edge': 8.37.1 '@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.9.3(@swc/helpers@0.5.13))) chalk: 3.0.0 - next: 15.0.4(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) + next: 15.0.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) resolve: 1.22.8 rollup: 3.29.5 stacktrace-parser: 0.1.10 @@ -18423,36 +18426,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.0.3(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4): - dependencies: - '@next/env': 15.0.3 - '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.13 - busboy: 1.6.0 - caniuse-lite: 1.0.30001678 - postcss: 8.4.31 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.6(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react@19.0.0) - optionalDependencies: - '@next/swc-darwin-arm64': 15.0.3 - '@next/swc-darwin-x64': 15.0.3 - '@next/swc-linux-arm64-gnu': 15.0.3 - '@next/swc-linux-arm64-musl': 15.0.3 - '@next/swc-linux-x64-gnu': 15.0.3 - '@next/swc-linux-x64-musl': 15.0.3 - '@next/swc-win32-arm64-msvc': 15.0.3 - '@next/swc-win32-x64-msvc': 15.0.3 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.48.1 - babel-plugin-react-compiler: 19.0.0-beta-df7b47d-20241124 - sass: 1.77.4 - sharp: 0.33.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - - next@15.0.4(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4): + next@15.0.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4): dependencies: '@next/env': 15.0.4 '@swc/counter': 0.1.3 @@ -20187,14 +20161,14 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - uploadthing@7.3.0(next@15.0.4(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)): + uploadthing@7.3.0(next@15.0.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)): dependencies: '@effect/platform': 0.69.8(effect@3.10.3) '@uploadthing/mime-types': 0.3.2 '@uploadthing/shared': 7.1.1 effect: 3.10.3 optionalDependencies: - next: 15.0.4(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) + next: 15.0.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.1)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4) uri-js@4.4.1: dependencies: