@cloudhal/schedules

中文文档
0.6.5发布于 2026/11/09Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 4889

Cloudhal schedules (RFC-0008): the time model, the RRULE subset and its sentences, releases (pin, closure, rl1: hash, compose, conflicts, state machine), automation and run state, taint and budget accounting, the Scheduler and runDue, and the SQLite/D1 st

原始使用文档
文档目录7 节
  1. Time (§9)
  2. Recurrence (§9.3, D3)
  3. Releases (§6)
  4. Identifiers (stable)
  5. Automations (§5.3, §11)
  6. Scheduler (§8)
  7. Built-in automations (§11.1, rollout step 9)

@cloudhal/schedules

Scheduled publishing and automations for Cloudhal sites (RFC-0008, ADR-0023): the time model, the RRULE subset, releases pinned to exact node versions, automation and run state, taint and budget accounting, and the scheduler's runDue with its D1/SQLite store.

Everything here is Worker-safe: no Node-only import at any entry. The only Temporal entry point is temporal-polyfill/implementation (never a native or global Temporal, never Temporal.Now; Biome refuses the other entries and a test scans the repository). now is always injected.

EntryWhat
@cloudhal/schedulesTime model, recurrence, release model, automation/run state machines, taint, budgets, TickMeter, the Scheduler interface, runDue, createScheduler. No storage dependency beyond the SqlDriver type.
@cloudhal/schedules/engineThe Temporal-free part: the release model, run ids, instant helpers, TickMeter, releaseFinishStatements, the scheduler's types. @cloudhal/core-ts applies releases through it, so the site Worker never bundles the time-zone library.
@cloudhal/schedules/timeThe time model alone; @cloudhal/core-ts/time re-exports it (one implementation).
@cloudhal/schedules/recurrenceThe RRULE subset alone (parse, validate, expand, presets, sentences).
@cloudhal/schedules/sqlSqlScheduleStore over SqlDriver for migration 0017_schedules.
@cloudhal/schedules/testingdescribeScheduleStoreConformance, describeSchedulerConformance, fakeReleaseExecutor, countingDriver, expectContract* (vitest).

Time (§9)

  • resolveLocalTime("2026-11-02T09:00", "Europe/Istanbul") → { at: "2026-11-02T06:00:00.000Z", offset: "+03:00" }. A time inside a DST gap moves to the next valid time (adjusted: "dst-gap"); a repeated time resolves to its first instant (D2).
  • recomputeExecuteAt(when) — never early: a stored wall-clock time is re-resolved with today's tzdata and the instant only ever moves later (adjusted: "tzdata").
  • isTimeZone, toLocalDateTime, zoneOffset, latenessSeconds, canonicalInstant (the stored form is Date#toISOString()).

Recurrence (§9.3, D3)

parseRule, formatRule, validateRecurrence (minimum interval 1 hour, at most 24 a day), occurrences, nextOccurrence, occurrencesBetween, presetRule/presetOf, presetMinuteOffset. Gap occurrences move forward instead of being dropped (the documented deviation from RFC 5545 §3.3.10); property tests compare the expander with rrule-temporal on seeded random rules with that difference excluded.

Sentences are data: describeRecurrence(rec) returns { key, times, days?, weekdays?, monthDays?, n?, count?, until? }; renderRecurrence(sentence, "en" | "tr") renders it with RECURRENCE_MESSAGES ("On the second Tuesday of every month at 09:07" / "Her ayın ikinci salısı 09:07").

Releases (§6)

  • pinItems, dependencyClosure (with referencedNodeIds as the default extractor), composeRelease (S1: nodes(S) = nodes(L) − ids + targets), releaseConflicts (S4), missingDependency, planRelease.
  • RELEASE_TRANSITIONS, HOLDING_STATES, TERMINAL_RELEASE_STATES, releaseWhen (lead time, horizon), baseApprovalVerdict (D5, D9), isPastLateLimit and nextRetryAt (the one-hour rule and back-off), orderReleases and releaseOverlaps (D15).

Identifiers (stable)

rl1   = "rl1:" + hex(SHA-256(utf8(lines.join("\n"))))
lines = ["site <siteId>"] + per item, sorted by node id (code-unit order):
        "<set|remove> <nodeId> <nodeKind> <targetRev>:<targetHash>|- <baseRev>:<baseHash>|-"

runId = "run_" + Crockford-base32(first 100 bits of SHA-256(utf8(subjectId + "\n" + occurrenceAt.toISOString())))

The time is not in rl1: (D4); an approval request's args_hash covers approvalArgs(releaseId, when). Release and automation ids are random (rel_/aut_ + 20 Crockford characters); lease tokens are random per claim.

Automations (§5.3, §11)

AUTOMATION_TRANSITIONS, RUN_TRANSITIONS, accountRunResult (three failures pause, a budget breach pauses at once), dueOccurrences (only the latest missed occurrence runs), runWindow, runEndsAt, killSwitchPauses/killSwitchResumes; taint (checkRunCall, afterRunCall, forbiddenUntrustedScopes, untrustedEffectiveScopes); budgets (checkRunBudget, budgetLeft, withinDailyWriteCap).

Scheduler (§8)

import { createScheduler, TICK_BUDGETS, TickMeter } from "@cloudhal/schedules";
import { SqlScheduleStore } from "@cloudhal/schedules/sql";

const scheduler = createScheduler({
  openStore: (meter) => new SqlScheduleStore(meter.wrap(d1Driver)),
  newMeter: (budget) => new TickMeter(budget),
  releases: releaseExecutor({ graph, schedules }), // `@cloudhal/core-ts/releases`
  builtins, systemJobs, checkApproval, wake: "minute",
});
await scheduler.runDue(new Date(controller.scheduledTime), TICK_BUDGETS.free, "cron");

In practice every ticker calls runScheduledTick from @cloudhal/core-ts/releases, which builds the graph, this store, the executor and the system jobs over one TickMeter. runClaimedRelease ends a run a caller claimed itself (a person's "Publish now", SqlScheduleStore.claimManualRun).

runDue claims each occurrence with a compare-and-swap and a fresh lease token and guards every later write on that token, so concurrent ticks produce one effect (S3). Releases run one at a time per site in time order (D15), at most an hour late (S8), with back-off retries and lease recovery. SqlScheduleStore.releaseFinishStatements(…, guard) gives publishRelease the statements that end a release to append to its own guarded batch.

For the API and MCP (rollout steps 7–8) the store also has automationByKey, openAgentRun, readyAgentRun, openManualWindow, recordRunCall, endAgentRunAs, updateAutomation, deleteAutomation, retitleRelease and releaseLateness. The scheduler's checkApproval is createReleaseApprovalCheck from @cloudhal/mcp-server/schedules (re-exported by @cloudhal/api with releaseRights).

Built-in automations (§11.1, rollout step 9)

createBuiltinRunner(sources) is the BuiltinRunner for the five jobs; @cloudhal/core-ts/automations implements BuiltinSources over the site graph (graphBuiltinRunner), and runScheduledTick uses it by default. Every job runs in bounded steps — one per tick, at most BUILTIN_STEP_COST[job] statements — and keeps its position in automations.cursor, so a large site spans several ticks and a crash between them continues the same run. Findings (Studio-only JSON on the run, at most 200; counts exact) carry stable codes:

JobStepsFinding codesCounts
broken-linkspages of 50 live pages, entries, terms and menus; then the week's 404 rows (taints: broken_links)broken-internal-link, broken-media-reference, broken-menu-target, link-to-gated-content, not-found-path (with a redirect proposal)per code, broken-links, nodes-scanned, links-checked
stale-contentpages of 200 entries (route fields only)stale-entry (oldest first)stale-entries, entries-scanned
feed-check/rss.xml, then /sitemap.xml (taints: feed_check)feed-unreachable, feed-invalid, feed-item-incomplete, feed-future-date, feed-missing-entry, feed-gated-node (the gate self-check), feed-asset-not-liveper code, documents-checked, feed-problems, feed-errors
weekly-digestonedigest-published, digest-scheduled, digest-attention, digest-open-findings (sections)published, scheduled, needs-attention, open-findings
cleanupretention batches until nothing is left, then remindersstale-draftruns-deleted, releases-deleted, findings-trimmed, leases-cleared, stale-runs-closed, stale-drafts

Every built-in run also records counts.steps and counts.statements (its budget use). A step that reads visitor text or a fetched document returns taint; the running run is marked at once. RunDueDeps.notify (ScheduleNotifier) receives automation-findings, automation-digest, automation-failed and automation-paused after the run's end is committed; Studio's notifications are derived from the run rows by @cloudhal/api. A person's "Run now" is SqlScheduleStore.startBuiltinNow: a manual run for this minute that the next tick steps, after which the automation returns to its schedule. Nothing seeds automations: people create them in Studio (D7).

Statements per path (counted by TickMeter, Workers Free budget 40):

PathStatements
Idle tick1 (2 when the heartbeat is due, every fifth minute)
Release published, executor not committing9
Release published, publishRelease shape (4 reads + 8-statement batch)18
Release published by runScheduledTick with the real publishRelease (core-ts suite, no asset blobs)17
Release missed7
Release recovered (expired lease or transient error)13
Built-in step (claim, a two-statement step, finish)8
Built-in step that taints the run (more to do)10
broken-links tick (core-ts suite: a page of live nodes)9–10
stale-content tick8–9
feed-check tick9
weekly-digest tick13
cleanup tick (retention batch / draft reminders)17 / 8
Agent-task occurrence recorded6
Agent-task window closed8
First tick registering system jobs3
System job run (plus the job's own)6

版本与发布记录

@cloudhal/schedules 共发布 26 个版本,首次发布于 2026/10/20;过去 12 个月发布了 26 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.5latest2026/11/091 天前
0.6.42026/11/081 天前
0.6.32026/11/081 天前
0.6.22026/10/2614 天前
0.6.12026/10/2417 天前
0.6.02026/10/2317 天前

关于 @cloudhal/schedules 的常见问题

@cloudhal/schedules 是什么?

Cloudhal schedules (RFC-0008): the time model, the RRULE subset and its sentences, releases (pin, closure, rl1: hash, compose, conflicts, state machine), automation and run state, taint and budget accounting, the Scheduler and runDue, and the SQLite/D1 st

如何安装 @cloudhal/schedules?

在项目目录运行 npm install @cloudhal/schedules,或使用 pnpm add / yarn add / bun add。

@cloudhal/schedules 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cloudhal/schedules 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cloudhal/schedules 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cloudhal/schedules 可以商用吗?

@cloudhal/schedules 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@cloudhal/schedules 的下载量有多少?

根据 npm 官方下载统计,@cloudhal/schedules 在 2026/10/10 – 2026/11/08 这 30 天内被下载 4,889 次,日均约 163 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cloudhal/schedules 还在维护吗?

最新版本 0.6.5 发布于 2026/11/09(1 天前);至今共发布 26 个版本,其中过去 12 个月发布了 26 个;首次发布于 2026/10/20。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。