已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

Cron

探索 Effect 中的 cron 调度:在特定时间与间隔执行操作。

Cron 模块让你可以用类似 UNIX cron 表达式 的风格定义调度。 它还支持部分约束(例如特定的月份或星期几)、通过 DateTime 模块实现的时区感知,以及健壮的错误处理。

这个模块可以帮助你:

  • 创建(Create):由各个单独的字段构造一个 Cron 实例。
  • 解析并校验(Parse and validate):解析 cron 表达式并校验其有效性。
  • 匹配(Match):检查已有日期是否满足给定的 cron 调度。
  • 查找(Find):找出给定日期之后该调度的下一次触发时间。
  • 迭代(Iterate):遍历符合某个调度的未来日期。
  • 转换(Convert):把 Cron 实例转换为 Schedule,以便在 effectful 程序中使用。

创建 Cron

你可以通过为秒、分、时、日、月、星期几指定数值约束来定义 cron 调度。make 函数要求你定义表示该调度约束的所有字段。

示例(创建 Cron)

import { Cron, DateTime } from "effect"

// Build a cron that triggers at 4:00 AM
// on the 8th to the 14th of each month
const cron = Cron.make({
  seconds: [0], // Trigger at the start of a minute
  minutes: [0], // Trigger at the start of an hour
  hours: [4], // Trigger at 4:00 AM
  days: [8, 9, 10, 11, 12, 13, 14], // Specific days of the month
  months: [], // No restrictions on the month
  weekdays: [], // No restrictions on the weekday
  tz: DateTime.zoneMakeNamedUnsafe("Europe/Rome"), // Optional time zone
})

const hours = [...cron.hours]
hours // => [4]
  • secondsminuteshours:定义一天中的时间。
  • daysmonths:指定哪些日历日和月份是有效的。
  • weekdays:把调度限制在一周中的特定几天。
  • tz:可选地为该调度指定时区。

如果某个字段留空(例如 months),它会被视为「无约束」,该日期部分可以取任意有效值。

解析 cron 表达式

除了手动构造 Cron,你也可以使用类 UNIX 的 cron 字符串,并用 parseparseUnsafe 解析它们。

parse

parse(cronExpression, tz?) 函数会安全地把 cron 字符串解析为 Cron 实例。它返回一个 Result,其中要么是解析得到的 Cron,要么是一个解析错误。

示例(安全地解析 cron 表达式)

import { Result, Cron } from "effect"

// Define a cron expression for 4:00 AM
// on the 8th to the 14th of every month
const expression = "0 0 4 8-14 * *"

// Parse the cron expression
const result = Cron.parse(expression)

if (Result.isSuccess(result)) {
  // Successfully parsed
  console.log("Parsed cron:", result.success)
} else {
  // Parsing failed
  console.error("Failed to parse cron:", result.failure.message)
}

Result.isSuccess(result) // => true

parseUnsafe

parseUnsafe(cronExpression, tz?) 函数的工作方式与 parse 类似,但当输入无效时它会抛出异常,而不是返回 Result

示例(解析 cron 表达式)

import { Cron } from "effect"

// Parse a cron expression for 4:00 AM
// on the 8th to the 14th of every month
// Throws if the expression is invalid
const cron = Cron.parseUnsafe("0 0 4 8-14 * *")

const hours = [...cron.hours]
hours // => [4]

用 match 检查日期

match 函数让你可以判断给定的 Date(或任意 DateTime.Input)是否满足某个 cron 调度的约束。

如果该日期满足调度的条件,match 返回 true;否则返回 false

示例(检查日期是否匹配 cron 调度)

import { Cron } from "effect"

// Suppose we have a cron that triggers at 4:00 AM
// on the 8th to the 14th of each month
const cron = Cron.parseUnsafe("0 0 4 8-14 * *")

const checkDate = new Date("2025-01-08 04:00:00")

console.log(Cron.match(cron, checkDate))
Cron.match(cron, checkDate) // => true

查找下一次运行时间

next 函数从指定日期开始,找出满足给定 cron 调度的下一个日期。如果没有提供起始日期,则以当前时间作为起点。

如果 next 在预定义的迭代次数内找不到匹配的日期,它会抛出错误,以避免无限循环。

示例(确定下一个匹配的日期)

import { Cron } from "effect"

// Define a cron expression for 4:00 AM
// on the 8th to the 14th of every month
const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")

// Specify the starting point for the search
const after = new Date("2025-01-08")

// Find the next matching date
const nextDate = Cron.next(cron, after)

console.log(nextDate)
nextDate.toISOString() // => "2025-01-08T04:00:00.000Z"

迭代未来的日期

要生成多个符合某个 cron 调度的未来日期,可以使用 sequence 函数。该函数会从指定日期开始,提供一个匹配日期的无限迭代器。

示例(用迭代器生成未来的日期)

import { Cron } from "effect"

// Define a cron expression for 4:00 AM
// on the 8th to the 14th of every month
const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")

// Specify the starting date
const start = new Date("2021-01-08")

// Create an iterator for the schedule
const iterator = Cron.sequence(cron, start)

// Get the first matching date after the start date
const first = iterator.next().value
console.log(first)
first?.toISOString() // => "2021-01-08T04:00:00.000Z"

// Get the second matching date after the start date
const second = iterator.next().value
console.log(second)
second?.toISOString() // => "2021-01-09T04:00:00.000Z"

转换为 Schedule

Schedule 模块让你可以定义重复发生的行为,例如重试或周期性事件。cron 函数在 Cron 模块与 Schedule 模块之间架起桥梁,让你能够基于 cron 表达式或 Cron 实例创建调度。

cron

Schedule.cron 函数会生成一个 Schedule,它在给定的 cron 表达式或 Cron 实例所定义的每个区间开始时触发。触发时,该调度会产出一个元组 [start, end],表示该 cron 区间窗口的时间戳(以毫秒为单位)。

示例(由 Cron 创建 Schedule)

import { Effect, Schedule, Fiber, Cron, Console, Duration } from "effect"
import { TestClock } from "effect/testing"

// A helper function to log output at each interval of the schedule
const log = <A>(
  action: Effect.Effect<A>,
  schedule: Schedule.Schedule<Duration.Duration, unknown, Cron.CronParseError>,
) => {
  let i = 0

  return Effect.gen(function* () {
    const fiber = yield* Effect.gen(function* () {
      yield* action
      i++
    }).pipe(
      Effect.repeat(
        schedule.pipe(
          // Limit the number of iterations for the example
          Schedule.upTo({ times: 10 }),
          Schedule.tap(({ now, output }) =>
            Console.log(
              i === 11 ? "..." : new Date(now + Duration.toMillis(output)),
            ),
          ),
        ),
      ),
      Effect.forkChild,
    )
    yield* TestClock.adjust(Infinity)
    yield* Fiber.join(fiber)
  }).pipe(Effect.provide(TestClock.layer()), Effect.runPromise)
}

// Build a cron that triggers at 4:00 AM
// on the 8th to the 14th of each month
const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC")

// Convert the Cron into a Schedule
const schedule = Schedule.cron(cron)

// Define a dummy action to repeat
const action = Effect.void

// Log the schedule intervals
await log(action, schedule)
/*
Output:
1970-01-08T04:00:00.000Z
1970-01-09T04:00:00.000Z
1970-01-10T04:00:00.000Z
1970-01-11T04:00:00.000Z
1970-01-12T04:00:00.000Z
1970-01-13T04:00:00.000Z
1970-01-14T04:00:00.000Z
1970-02-08T04:00:00.000Z
1970-02-09T04:00:00.000Z
1970-02-10T04:00:00.000Z
...
*/

// The schedule fires at the same instants the Cron itself reports,
// starting from the Unix epoch (TestClock starts at time 0)
const iterator = Cron.sequence(cron, new Date(0))
const firstTen = Array.from({ length: 10 }, () =>
  iterator.next().value?.toISOString(),
)
firstTen // => ["1970-01-08T04:00:00.000Z", "1970-01-09T04:00:00.000Z", "1970-01-10T04:00:00.000Z", "1970-01-11T04:00:00.000Z", "1970-01-12T04:00:00.000Z", "1970-01-13T04:00:00.000Z", "1970-01-14T04:00:00.000Z", "1970-02-08T04:00:00.000Z", "1970-02-09T04:00:00.000Z", "1970-02-10T04:00:00.000Z"]
Using a Real Clock

在真实的应用中,你不需要使用 TestClockTestContext。它们只在测试环境中模拟时间、 控制执行时才需要。