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]
seconds、minutes和hours:定义一天中的时间。days和months:指定哪些日历日和月份是有效的。weekdays:把调度限制在一周中的特定几天。tz:可选地为该调度指定时区。
如果某个字段留空(例如 months),它会被视为「无约束」,该日期部分可以取任意有效值。
解析 cron 表达式
除了手动构造 Cron,你也可以使用类 UNIX 的 cron 字符串,并用 parse 或 parseUnsafe 解析它们。
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"]
在真实的应用中,你不需要使用 TestClock 或
TestContext。它们只在测试环境中模拟时间、
控制执行时才需要。