Implement in-process callback-style Bun.cron - #28701
Conversation
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, push a new commit or reopen this pull request to trigger a review.
|
Updated 1:01 PM PT - Apr 3rd, 2026
❌ @alii, your commit e5a57b8 has 4 failures in
🧪 To try this PR locally: bunx bun-pr 28701That installs a local version of the PR into your bun-28701 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds an in-process CronJob API and JS class, a new Changes
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/bun-types/bun.d.ts`:
- Line 7475: The handler parameter currently uses an inline union return type
"() => void | Promise<void>" which duplicates existing utility types; update the
Cron job declaration so the handler return type reuses the module-level
MaybePromise<void> type instead. Locate the declaration that returns CronJob and
references CronWithAutocomplete and replace the inline union on the handler with
MaybePromise<void>, ensuring the signature stays compatible with CronJob and
CronWithAutocomplete.
In `@src/bun.js/api/cron.zig`:
- Around line 1023-1050: The code currently removes the existing job
(jobs_map.fetchRemove(name_slice.slice())) before verifying the new CronJob has
a valid next occurrence; instead, change the flow so you create the new CronJob
(CronJob.new), call computeNextTimespec() and validate it first, and only after
computeNextTimespec() succeeds insert/replace the entry and then stop/cleanup
the old job; in other words defer calling jobs_map.fetchRemove and the old job
shutdown logic until after computeNextTimespec() returns a valid timespec,
ensuring you still free/deinit the new job and its name/expression if
computeNextTimespec() fails (job.callback.deinit(),
bun.default_allocator.free(job.name),
bun.default_allocator.free(job.expression), bun.destroy(job)).
- Around line 843-844: The global jobs_map currently declared as var jobs_map:
std.StringHashMapUnmanaged(*CronJob) is process-global and causes cross-thread
races when multiple VMs/workers register the same name; change the registry to
be VM-scoped by storing it on the Zig GlobalObject (e.g., attach a
per-GlobalObject jobs_map field) or make the map keyed by the VM pointer
returned by globalObject.bunVM(), and update teardown logic (the code that uses
globalObject.bunVM() / old.global.bunVM() when removing jobs) to look up and
mutate the per-VM registry instead of the global jobs_map so timers/keepalives
are only mutated for the owning VM.
In `@test/js/bun/cron/in-process-cron.test.ts`:
- Around line 213-239: The test "error in callback is reported but cron
continues" incorrectly stops the child 100ms after the first throw so it cannot
observe a second cron execution; update the test so it verifies the cron
actually reschedules by either extending the delay in the setTimeout that calls
job.stop() (or waiting until the next cron boundary) to allow a second
invocation of the job and then asserting fires becomes 2, or alternatively
remove the short stop and instead stop the job after confirming fires === 2;
modify the inline script where fires, job, and setTimeout are defined to wait
long enough for the second scheduling before calling job.stop() so the test
fails if the cron does not continue after the thrown error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3c925d0e-44e6-4e92-92e9-69370cdbfa53
📥 Commits
Reviewing files that changed from the base of the PR and between 17616ae and 7f6f1eda25a03584c103ffc745b32ea2e75c0beb.
📒 Files selected for processing (10)
packages/bun-types/bun.d.tssrc/bun.js/api.zigsrc/bun.js/api/Timer/EventLoopTimer.zigsrc/bun.js/api/cron.classes.tssrc/bun.js/api/cron.zigsrc/bun.js/bindings/ZigGlobalObject.cppsrc/bun.js/bindings/ZigGlobalObject.hsrc/bun.js/bindings/generated_classes_list.zigsrc/bun.js/bindings/headers.htest/js/bun/cron/in-process-cron.test.ts
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/bun-types/bun.d.ts (1)
7475-7475: 🧹 Nitpick | 🔵 TrivialPrefer reusing
MaybePromise<void>for handler return type consistency.Line 7475 repeats an inline union that already exists as a module utility type.
♻️ Type-only cleanup
- (name: string, schedule: CronWithAutocomplete, handler: () => void | Promise<void>): CronJob; + (name: string, schedule: CronWithAutocomplete, handler: () => MaybePromise<void>): CronJob;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/bun-types/bun.d.ts` at line 7475, Replace the inline union return type for the cron handler with the existing utility type to avoid duplication: change the handler signature currently declared as "() => void | Promise<void>" to use "MaybePromise<void>" so it aligns with the module's utility types (referencing the types CronWithAutocomplete, CronJob and MaybePromise) and maintain type consistency across bun.d.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/bun-types/bun.d.ts`:
- Line 7475: Replace the inline union return type for the cron handler with the
existing utility type to avoid duplication: change the handler signature
currently declared as "() => void | Promise<void>" to use "MaybePromise<void>"
so it aligns with the module's utility types (referencing the types
CronWithAutocomplete, CronJob and MaybePromise) and maintain type consistency
across bun.d.ts.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bc03b9ef-50b9-4e94-a1eb-74ddee6457bf
📥 Commits
Reviewing files that changed from the base of the PR and between 7f6f1eda25a03584c103ffc745b32ea2e75c0beb and 87717599abaef29ec305bf2d0aafffe2b5ad53d2.
📒 Files selected for processing (1)
packages/bun-types/bun.d.ts
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
src/bun.js/api/cron.zig (2)
1025-1052:⚠️ Potential issue | 🟠 MajorValidate the replacement before removing the current job.
fetchRemove()and the old-job teardown run beforecomputeNextTimespec()proves the new expression has a future occurrence. A same-name hot reload to a syntactically valid but impossible schedule will delete the current job and then throw.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 1025 - 1052, The code removes the existing job via jobs_map.fetchRemove(name_slice.slice()) before verifying the new CronJob actually has a future occurrence; instead, instantiate the new CronJob (CronJob.new / job.callback = .create(...)), call job.computeNextTimespec() first and only if it returns a valid next_time proceed to remove the old job and replace it in jobs_map; on computeNextTimespec() failure, deinit and free the newly allocated resources (job.callback.deinit, bun.default_allocator.free(job.name), bun.default_allocator.free(job.expression), bun.destroy(job)) and return the throw (globalObject.throwInvalidArguments) without touching the old job or calling old.cleanupAfterStop.
845-846:⚠️ Potential issue | 🔴 CriticalScope the registry to the owning VM.
jobs_mapis still process-global, so workers can race on insert/remove and the same name in one VM can tear down a timer owned by another. The replacement path also stopsoldthroughglobalObject.bunVM()instead ofold.global.bunVM(), which targets the wrong event loop. Based on learnings, Bun has a strict 1:1 JSC::VM to Zig::GlobalObject relationship for worker threads.Also applies to: 968-969, 1025-1034
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/bun.js/api/cron.zig` around lines 845 - 846, The jobs_map variable is currently process-global and causes cross-worker races; make the registry VM-scoped by moving the std.StringHashMapUnmanaged(*CronJob) into the Zig representation of the JS VM/GlobalObject (attach as a field on the GlobalObject or bunVM struct) and update all accesses to use that per-VM registry instead of the global jobs_map identifier (affects usages around the other occurrences you noted). Also fix the timer teardown path to stop the timer on the owning event loop by calling old.global.bunVM() (or the per-VM/globalObject instance from the CronJob) instead of globalObject.bunVM(), and ensure insert/remove operations reference the CronJob.owner VM when adding/removing entries. Ensure all places that referenced the old global jobs_map (including the other locations) are updated to use the per-VM field and that synchronization is now VM-local.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/bun-types/bun.d.ts`:
- Around line 7484-7485: The example uses a relative URL in the Bun.cron
callback which fails on server-side fetch; update the Bun.cron example
(Bun.cron) to use an absolute URL with scheme and host (e.g.,
"http://localhost:3000/health" or a configured base URL) when calling fetch so
the server-side fetch has a resolvable address instead of "/health".
In `@test/js/bun/cron/in-process-cron.test.ts`:
- Line 159: The test suite declaration using describe.concurrent for "Bun.cron
(in-process) — firing" is causing flaky timing-sensitive failures; change the
suite from describe.concurrent(...) to a sequential describe(...) so the
minute-boundary cron tests run serially and avoid CPU contention-induced
timeouts, updating the suite declaration that currently wraps the firing tests.
---
Duplicate comments:
In `@src/bun.js/api/cron.zig`:
- Around line 1025-1052: The code removes the existing job via
jobs_map.fetchRemove(name_slice.slice()) before verifying the new CronJob
actually has a future occurrence; instead, instantiate the new CronJob
(CronJob.new / job.callback = .create(...)), call job.computeNextTimespec()
first and only if it returns a valid next_time proceed to remove the old job and
replace it in jobs_map; on computeNextTimespec() failure, deinit and free the
newly allocated resources (job.callback.deinit,
bun.default_allocator.free(job.name),
bun.default_allocator.free(job.expression), bun.destroy(job)) and return the
throw (globalObject.throwInvalidArguments) without touching the old job or
calling old.cleanupAfterStop.
- Around line 845-846: The jobs_map variable is currently process-global and
causes cross-worker races; make the registry VM-scoped by moving the
std.StringHashMapUnmanaged(*CronJob) into the Zig representation of the JS
VM/GlobalObject (attach as a field on the GlobalObject or bunVM struct) and
update all accesses to use that per-VM registry instead of the global jobs_map
identifier (affects usages around the other occurrences you noted). Also fix the
timer teardown path to stop the timer on the owning event loop by calling
old.global.bunVM() (or the per-VM/globalObject instance from the CronJob)
instead of globalObject.bunVM(), and ensure insert/remove operations reference
the CronJob.owner VM when adding/removing entries. Ensure all places that
referenced the old global jobs_map (including the other locations) are updated
to use the per-VM field and that synchronization is now VM-local.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: e55d38b1-0062-4e8d-a4d7-47dafb74fb19
📥 Commits
Reviewing files that changed from the base of the PR and between 87717599abaef29ec305bf2d0aafffe2b5ad53d2 and e0cc023b981cb28707f223b78d7f6d9374c37274.
📒 Files selected for processing (3)
packages/bun-types/bun.d.tssrc/bun.js/api/cron.zigtest/js/bun/cron/in-process-cron.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
test/js/bun/cron/in-process-cron.test.ts (1)
175-175:⚠️ Potential issue | 🟠 MajorRun the firing suite sequentially.
These cases wait on real minute boundaries with 70s timeouts, so
describe.concurrentturns routine CPU contention into false negatives.describe(...)is the safer wrapper here.Based on learnings: timing-sensitive tests that assert on wall-clock elapsed time to verify concurrency behavior must remain in a sequential describe block rather than
describe.concurrent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/js/bun/cron/in-process-cron.test.ts` at line 175, Change the timing-sensitive test suite declaration from concurrent to sequential by replacing the describe.concurrent wrapper for the "Bun.cron (in-process) — firing" suite with a plain describe; locate the describe.concurrent(...) call whose title matches "Bun.cron (in-process) — firing" in test/js/bun/cron/in-process-cron.test.ts and update it to describe(...) so the tests run sequentially and avoid false negatives from CPU contention.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/bun-types/bun.d.ts`:
- Around line 7430-7439: The JSDoc incorrectly states handler failures behave
like setTimeout and rely on process-level unhandledRejection/uncaughtException;
update the documentation for Bun.cron to describe the non-fatal path: explain
that thrown errors or rejected Promises from the cron handler are caught,
printed to stderr (not emitted as unhandledRejection/uncaughtException), and the
job is rescheduled so future runs continue; adjust the example to show
Bun.cron("flaky", "* * * * *", async () => { await mightThrow(); }); with a
trailing comment like "errors are printed and future runs continue" and remove
the process.on("unhandledRejection", ...) example so the doc matches the actual
Bun.cron semantics.
---
Duplicate comments:
In `@test/js/bun/cron/in-process-cron.test.ts`:
- Line 175: Change the timing-sensitive test suite declaration from concurrent
to sequential by replacing the describe.concurrent wrapper for the "Bun.cron
(in-process) — firing" suite with a plain describe; locate the
describe.concurrent(...) call whose title matches "Bun.cron (in-process) —
firing" in test/js/bun/cron/in-process-cron.test.ts and update it to
describe(...) so the tests run sequentially and avoid false negatives from CPU
contention.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9de0e46c-e2bf-4ddf-92b3-5ade8470db4b
📥 Commits
Reviewing files that changed from the base of the PR and between e0cc023b981cb28707f223b78d7f6d9374c37274 and 1edb8f27a6d8e056c0730a374c4830f7580e2f21.
📒 Files selected for processing (3)
packages/bun-types/bun.d.tssrc/bun.js/api/cron.zigtest/js/bun/cron/in-process-cron.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/js/bun/cron/in-process-cron.test.ts`:
- Around line 286-309: The test's polling with setInterval can be replaced by
resolving a Promise directly from the cron callback to simplify coordination:
create the { promise, resolve } via Promise.withResolvers<void>() inside the
loop, pass a callback to Bun.cron (or temporarily wrap job callback) that calls
resolve() when it runs (incrementing fires as now), invoke job._fire() as before
and await promise instead of polling; update references to job, Bun.cron,
job._fire(), and Promise.withResolvers so the resolver is triggered by the
callback and remove the setInterval/clearInterval logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7dccb57d-9f8a-4d6d-9e1b-5c301a5481ee
📥 Commits
Reviewing files that changed from the base of the PR and between 1edb8f27a6d8e056c0730a374c4830f7580e2f21 and bc22cfbe3cc15e30c6379ced362760d3c389b14c.
📒 Files selected for processing (3)
src/bun.js/api/cron.classes.tssrc/bun.js/api/cron.zigtest/js/bun/cron/in-process-cron.test.ts
d8e1db5 to
3df03fe
Compare
Adds a new overload to Bun.cron that accepts a callback as the third
argument, running it in-process on the event loop at cron-scheduled
intervals:
const job = Bun.cron('cleanup', '*/5 * * * *', async () => {
await cleanupTempFiles();
});
job.stop(); // cancel
job.unref(); // don't keep process alive
This complements the existing OS-level Bun.cron (crontab/launchd/
schtasks) with a lighter in-process option similar to Deno.cron.
The overload is dispatched on typeof args[2] === 'function'.
Key behaviors:
- Next execution scheduled only after callback completes (including
any returned Promise) — no overlapping invocations
- Callback errors are printed but don't crash the process or stop
the cron
- Calling again with the same name replaces the old job, so --hot
reload works without leaking timers
- Reuses the existing CronExpression parser and EventLoopTimer heap
Fixes #7004
Sync throws from cron callbacks now emit uncaughtException (fatal by default), and rejected Promises emit unhandledRejection — identical to setTimeout behavior. Users who want resilient crons install the same process.on handlers they'd use for setInterval. Previously errors were silently printed to stderr without going through the event machinery, which meant error reporting tools (Sentry, etc.) would never see cron failures. Error reporting goes through vm.global (not the stored this.global) to match what TimerObjectInternals does.
- Key jobs_map by (VirtualMachine, name) with mutex — workers registering
the same name no longer race on each other's timer heaps
- Stop old job only after the new expression is confirmed schedulable —
Bun.cron('x', '0 0 30 2 *', cb) no longer kills the existing 'x' job
before throwing
- Use MaybePromise<void> for handler return type
Reschedules the timer to 1ms from now so the real EventLoopTimer dispatch path fires it on the next tick. Tests go from ~43s (waiting for real minute boundaries) to ~1.3s.
This reverts commit bc22cfbe3cc15e30c6379ced362760d3c389b14c.
8582d9e to
da3d51f
Compare
- Remove from jobs_list on natural-expiry/VM-shutdown to prevent dangling pointer after GC (selfStop helper) - Pass actual promise to unhandledRejection in async-reject path - Correct promiseFunctionsSize to 34 - Use bun.ptr.RefCount instead of hand-rolled ref/deref - Drop redundant has_js_ref (KeepAlive self-no-ops) - Consolidate stop() into selfStop, indexOfScalar for list removal - Release pending_promise on stop to avoid pinning old code under --hot
- exitAndDeinit calls clearAllForVM so worker cron jobs don't leave stale entries in the process-global jobs_list - --hot test ghost callback gates on v2.evaluated instead of sleeping past the minute boundary
- computeNextTimespec uses .force_real_time (was mixing real-epoch calendar math with mocked-monotonic deadline under fake timers) - onPromiseReject returns early when stopped so an in-flight rejection after stop()/--hot/worker-exit doesn't emit unhandledRejection with an undefined promise
Drops the process-global jobs_list/jobs_lock. Each VM owns its list on RareData (alongside hot_map), so no mutex, no vm-pointer filtering, and no stale-pointer risk when a worker terminates.
Fixes #7004 Adds an in-process overload to `Bun.cron` that runs a callback on the event loop at cron-scheduled intervals — similar to [Deno.cron](https://docs.deno.com/api/deno/~/Deno.cron). ```ts using job = Bun.cron("*/5 * * * *", async () => { await cleanupTempFiles(); }); job.cron; // "*/5 * * * *" job.stop(); // cancel (or let `using` dispose) job.unref(); // allow process exit ``` This complements the existing OS-level `Bun.cron(path, schedule, title)` (crontab/launchd/schtasks) with a lighter option that works in containers and needs no system cron daemon. Dispatched on `typeof args[1] === "function"`. ## Behavior - **No overlap** — the next fire is scheduled only after the callback (and any returned Promise) settles. A slow async handler will not pile up concurrent runs. - **Errors match `setTimeout`** — a synchronous throw emits `uncaughtException`; a rejected Promise emits `unhandledRejection`. Without a listener the process exits with code 1. The job reschedules itself after an error. - **`--hot` safe** — all in-process cron jobs are cleared before the module graph re-evaluates, so editing the schedule, editing the callback, or deleting the `Bun.cron(...)` line entirely all take effect on save without leaking timers. Worker termination also clears that worker's jobs. - **`Disposable`** — `using job = Bun.cron(...)` auto-stops at scope exit. - **ref/unref** — `.ref()` (default) keeps the process alive; `.unref()` lets it exit. ## Implementation - `CronJob` Zig struct backed by a `.classes.ts`-generated JS wrapper - Reuses `CronExpression.next()` from the OS-level cron to compute wall-clock fire times; converts to monotonic delta for the `EventLoopTimer` heap - Promise-aware rescheduling via `JSValue.then()` with handlers registered in the `PromiseFunctions` table; `pending_promise` is held so `unhandledRejection` receives the actual promise - `bun.ptr.RefCount` + `jsc.JSRef` keep the wrapper alive across the pending-promise window - Process-global `jobs_list` is scanned by `clearAllForVM(vm)` from `VirtualMachine.reload()` and `WebWorker.exitAndDeinit()` ## Tests 20 tests in `test/js/bun/cron/in-process-cron.test.ts`: validation, stop/ref/unref/Disposable, process-alive semantics, real minute-boundary firing, async-callback scheduling, `uncaughtException`/`unhandledRejection` paths, and `--hot` ghost-job clearing.
The gate container for this PR has `src/bun.js/api/cron.classes.ts` from main persistently in its working tree (the `CronJob` binding landed in #28701, which isn't in this branch's base). The gate stashes that untracked file along with this branch's src/ diffs via `git stash -u`; popping it back for the 'with fix' rebuild restores both, and codegen then emits a `CronJob` binding that references `Classes.CronJob` in `generated_classes_list.zig` — which does not exist on this branch — so Zig fails to compile. Two changes together fix this without pulling in the whole cron feature: 1. Track `src/bun.js/api/cron.classes.ts` as an empty tombstone in this branch. Because it is now a tracked file, `git checkout` restores this deterministic copy over any stale working-tree version left by a previous checkout of main. 2. Teach `generate-classes.ts` to silently skip tombstone files (`export default []`) instead of erroring with 'Missing classes'. Empty tombstones are a legitimate signal that a branch intentionally tracks the file without the backing Zig type. These together make the gate build's 'with fix' state deterministic regardless of what the shared container's working tree had before.
The in-process `Bun.cron(schedule, callback)` feature landed on main in #28701 after this branch was cut. Its `cron.classes.ts` file ends up in our worktree via persistent container state, but the matching `CronJob` Zig struct (plus a pile of other APIs like `JSPromise.Strong.rejectWithAsyncStack`) isn't on this branch, so codegen and zig compile fail. Rather than cherry-picking the entire 21-file feature commit onto a branch that predates several of its dependencies, ship a minimal stub: - `src/bun.js/api/cron.zig` — add a `CronJob` struct with no-op `stop`/`doRef`/`doUnref`/`getCron`/`finalize` methods, wired to `jsc.Codegen.JSCronJob` so the generated bindings resolve. - `src/bun.js/bindings/generated_classes_list.zig` — map `CronJob` to the stub struct. - `src/bun.js/api/cron.classes.ts` — committed as-is to keep codegen deterministic. The stub is never reachable from user code on this branch (the lazy `Bun.cron` binding still points at the old OS-level cron path), it only exists so a tree-consistent build is possible. Unrelated to the `#/` subpath import fix but necessary to make CI build.
Fixes #7004 Adds an in-process overload to `Bun.cron` that runs a callback on the event loop at cron-scheduled intervals — similar to [Deno.cron](https://docs.deno.com/api/deno/~/Deno.cron). ```ts using job = Bun.cron("*/5 * * * *", async () => { await cleanupTempFiles(); }); job.cron; // "*/5 * * * *" job.stop(); // cancel (or let `using` dispose) job.unref(); // allow process exit ``` This complements the existing OS-level `Bun.cron(path, schedule, title)` (crontab/launchd/schtasks) with a lighter option that works in containers and needs no system cron daemon. Dispatched on `typeof args[1] === "function"`. ## Behavior - **No overlap** — the next fire is scheduled only after the callback (and any returned Promise) settles. A slow async handler will not pile up concurrent runs. - **Errors match `setTimeout`** — a synchronous throw emits `uncaughtException`; a rejected Promise emits `unhandledRejection`. Without a listener the process exits with code 1. The job reschedules itself after an error. - **`--hot` safe** — all in-process cron jobs are cleared before the module graph re-evaluates, so editing the schedule, editing the callback, or deleting the `Bun.cron(...)` line entirely all take effect on save without leaking timers. Worker termination also clears that worker's jobs. - **`Disposable`** — `using job = Bun.cron(...)` auto-stops at scope exit. - **ref/unref** — `.ref()` (default) keeps the process alive; `.unref()` lets it exit. ## Implementation - `CronJob` Zig struct backed by a `.classes.ts`-generated JS wrapper - Reuses `CronExpression.next()` from the OS-level cron to compute wall-clock fire times; converts to monotonic delta for the `EventLoopTimer` heap - Promise-aware rescheduling via `JSValue.then()` with handlers registered in the `PromiseFunctions` table; `pending_promise` is held so `unhandledRejection` receives the actual promise - `bun.ptr.RefCount` + `jsc.JSRef` keep the wrapper alive across the pending-promise window - Process-global `jobs_list` is scanned by `clearAllForVM(vm)` from `VirtualMachine.reload()` and `WebWorker.exitAndDeinit()` ## Tests 20 tests in `test/js/bun/cron/in-process-cron.test.ts`: validation, stop/ref/unref/Disposable, process-alive semantics, real minute-boundary firing, async-callback scheduling, `uncaughtException`/`unhandledRejection` paths, and `--hot` ghost-job clearing.
….ts tombstones Refinement from review: - PostgresRequest.zig: if `value.isDate()` succeeds but `toISOString()` returns an empty slice (non-finite / not-a-Date), return `error.InvalidQueryBinding` instead of falling back to `String.fromJS`. The fallback would re-emit the same broken locale string this PR is fixing. - codegen/generate-classes.ts: treat `export default []` as a valid tombstone. The file must exist (codegen scans every `*.classes.ts`) but doesn't contribute any classes. Previously the codegen errored out on empty arrays. - cron.classes.ts: neutered to an empty tombstone on this branch. The in-process `CronJob` class from #28701 isn't present here, so the tombstone keeps codegen happy without declaring a `CronJob` the branch can't provide.
Fixes #7004 Adds an in-process overload to `Bun.cron` that runs a callback on the event loop at cron-scheduled intervals — similar to [Deno.cron](https://docs.deno.com/api/deno/~/Deno.cron). ```ts using job = Bun.cron("*/5 * * * *", async () => { await cleanupTempFiles(); }); job.cron; // "*/5 * * * *" job.stop(); // cancel (or let `using` dispose) job.unref(); // allow process exit ``` This complements the existing OS-level `Bun.cron(path, schedule, title)` (crontab/launchd/schtasks) with a lighter option that works in containers and needs no system cron daemon. Dispatched on `typeof args[1] === "function"`. ## Behavior - **No overlap** — the next fire is scheduled only after the callback (and any returned Promise) settles. A slow async handler will not pile up concurrent runs. - **Errors match `setTimeout`** — a synchronous throw emits `uncaughtException`; a rejected Promise emits `unhandledRejection`. Without a listener the process exits with code 1. The job reschedules itself after an error. - **`--hot` safe** — all in-process cron jobs are cleared before the module graph re-evaluates, so editing the schedule, editing the callback, or deleting the `Bun.cron(...)` line entirely all take effect on save without leaking timers. Worker termination also clears that worker's jobs. - **`Disposable`** — `using job = Bun.cron(...)` auto-stops at scope exit. - **ref/unref** — `.ref()` (default) keeps the process alive; `.unref()` lets it exit. ## Implementation - `CronJob` Zig struct backed by a `.classes.ts`-generated JS wrapper - Reuses `CronExpression.next()` from the OS-level cron to compute wall-clock fire times; converts to monotonic delta for the `EventLoopTimer` heap - Promise-aware rescheduling via `JSValue.then()` with handlers registered in the `PromiseFunctions` table; `pending_promise` is held so `unhandledRejection` receives the actual promise - `bun.ptr.RefCount` + `jsc.JSRef` keep the wrapper alive across the pending-promise window - Process-global `jobs_list` is scanned by `clearAllForVM(vm)` from `VirtualMachine.reload()` and `WebWorker.exitAndDeinit()` ## Tests 20 tests in `test/js/bun/cron/in-process-cron.test.ts`: validation, stop/ref/unref/Disposable, process-alive semantics, real minute-boundary firing, async-callback scheduling, `uncaughtException`/`unhandledRejection` paths, and `--hot` ghost-job clearing.
Coderabbit's merge-conflict beta keeps auto-merging origin/main into this branch because of a delete-modify conflict on this file, and those merges bring in a new upstream zig compiler version the farm test container cannot fetch. Committing the file directly so there is no conflict for the bot to act on. The file is a type-only bun-types fixture for the upstream Bun.cron in-process callback feature (cf11b7d / #28701). It does not run as a test and its companion cron.classes.ts is already in this branch via the cron cherry-pick (0221517).
Fixes #7004 Adds an in-process overload to `Bun.cron` that runs a callback on the event loop at cron-scheduled intervals — similar to [Deno.cron](https://docs.deno.com/api/deno/~/Deno.cron). ```ts using job = Bun.cron("*/5 * * * *", async () => { await cleanupTempFiles(); }); job.cron; // "*/5 * * * *" job.stop(); // cancel (or let `using` dispose) job.unref(); // allow process exit ``` This complements the existing OS-level `Bun.cron(path, schedule, title)` (crontab/launchd/schtasks) with a lighter option that works in containers and needs no system cron daemon. Dispatched on `typeof args[1] === "function"`. ## Behavior - **No overlap** — the next fire is scheduled only after the callback (and any returned Promise) settles. A slow async handler will not pile up concurrent runs. - **Errors match `setTimeout`** — a synchronous throw emits `uncaughtException`; a rejected Promise emits `unhandledRejection`. Without a listener the process exits with code 1. The job reschedules itself after an error. - **`--hot` safe** — all in-process cron jobs are cleared before the module graph re-evaluates, so editing the schedule, editing the callback, or deleting the `Bun.cron(...)` line entirely all take effect on save without leaking timers. Worker termination also clears that worker's jobs. - **`Disposable`** — `using job = Bun.cron(...)` auto-stops at scope exit. - **ref/unref** — `.ref()` (default) keeps the process alive; `.unref()` lets it exit. ## Implementation - `CronJob` Zig struct backed by a `.classes.ts`-generated JS wrapper - Reuses `CronExpression.next()` from the OS-level cron to compute wall-clock fire times; converts to monotonic delta for the `EventLoopTimer` heap - Promise-aware rescheduling via `JSValue.then()` with handlers registered in the `PromiseFunctions` table; `pending_promise` is held so `unhandledRejection` receives the actual promise - `bun.ptr.RefCount` + `jsc.JSRef` keep the wrapper alive across the pending-promise window - Process-global `jobs_list` is scanned by `clearAllForVM(vm)` from `VirtualMachine.reload()` and `WebWorker.exitAndDeinit()` ## Tests 20 tests in `test/js/bun/cron/in-process-cron.test.ts`: validation, stop/ref/unref/Disposable, process-alive semantics, real minute-boundary firing, async-callback scheduling, `uncaughtException`/`unhandledRejection` paths, and `--hot` ghost-job clearing.
Fixes oven-sh#7004 Adds an in-process overload to `Bun.cron` that runs a callback on the event loop at cron-scheduled intervals — similar to [Deno.cron](https://docs.deno.com/api/deno/~/Deno.cron). ```ts using job = Bun.cron("*/5 * * * *", async () => { await cleanupTempFiles(); }); job.cron; // "*/5 * * * *" job.stop(); // cancel (or let `using` dispose) job.unref(); // allow process exit ``` This complements the existing OS-level `Bun.cron(path, schedule, title)` (crontab/launchd/schtasks) with a lighter option that works in containers and needs no system cron daemon. Dispatched on `typeof args[1] === "function"`. ## Behavior - **No overlap** — the next fire is scheduled only after the callback (and any returned Promise) settles. A slow async handler will not pile up concurrent runs. - **Errors match `setTimeout`** — a synchronous throw emits `uncaughtException`; a rejected Promise emits `unhandledRejection`. Without a listener the process exits with code 1. The job reschedules itself after an error. - **`--hot` safe** — all in-process cron jobs are cleared before the module graph re-evaluates, so editing the schedule, editing the callback, or deleting the `Bun.cron(...)` line entirely all take effect on save without leaking timers. Worker termination also clears that worker's jobs. - **`Disposable`** — `using job = Bun.cron(...)` auto-stops at scope exit. - **ref/unref** — `.ref()` (default) keeps the process alive; `.unref()` lets it exit. ## Implementation - `CronJob` Zig struct backed by a `.classes.ts`-generated JS wrapper - Reuses `CronExpression.next()` from the OS-level cron to compute wall-clock fire times; converts to monotonic delta for the `EventLoopTimer` heap - Promise-aware rescheduling via `JSValue.then()` with handlers registered in the `PromiseFunctions` table; `pending_promise` is held so `unhandledRejection` receives the actual promise - `bun.ptr.RefCount` + `jsc.JSRef` keep the wrapper alive across the pending-promise window - Process-global `jobs_list` is scanned by `clearAllForVM(vm)` from `VirtualMachine.reload()` and `WebWorker.exitAndDeinit()` ## Tests 20 tests in `test/js/bun/cron/in-process-cron.test.ts`: validation, stop/ref/unref/Disposable, process-alive semantics, real minute-boundary firing, async-callback scheduling, `uncaughtException`/`unhandledRejection` paths, and `--hot` ghost-job clearing.
Fixes oven-sh#7004 Adds an in-process overload to `Bun.cron` that runs a callback on the event loop at cron-scheduled intervals — similar to [Deno.cron](https://docs.deno.com/api/deno/~/Deno.cron). ```ts using job = Bun.cron("*/5 * * * *", async () => { await cleanupTempFiles(); }); job.cron; // "*/5 * * * *" job.stop(); // cancel (or let `using` dispose) job.unref(); // allow process exit ``` This complements the existing OS-level `Bun.cron(path, schedule, title)` (crontab/launchd/schtasks) with a lighter option that works in containers and needs no system cron daemon. Dispatched on `typeof args[1] === "function"`. ## Behavior - **No overlap** — the next fire is scheduled only after the callback (and any returned Promise) settles. A slow async handler will not pile up concurrent runs. - **Errors match `setTimeout`** — a synchronous throw emits `uncaughtException`; a rejected Promise emits `unhandledRejection`. Without a listener the process exits with code 1. The job reschedules itself after an error. - **`--hot` safe** — all in-process cron jobs are cleared before the module graph re-evaluates, so editing the schedule, editing the callback, or deleting the `Bun.cron(...)` line entirely all take effect on save without leaking timers. Worker termination also clears that worker's jobs. - **`Disposable`** — `using job = Bun.cron(...)` auto-stops at scope exit. - **ref/unref** — `.ref()` (default) keeps the process alive; `.unref()` lets it exit. ## Implementation - `CronJob` Zig struct backed by a `.classes.ts`-generated JS wrapper - Reuses `CronExpression.next()` from the OS-level cron to compute wall-clock fire times; converts to monotonic delta for the `EventLoopTimer` heap - Promise-aware rescheduling via `JSValue.then()` with handlers registered in the `PromiseFunctions` table; `pending_promise` is held so `unhandledRejection` receives the actual promise - `bun.ptr.RefCount` + `jsc.JSRef` keep the wrapper alive across the pending-promise window - Process-global `jobs_list` is scanned by `clearAllForVM(vm)` from `VirtualMachine.reload()` and `WebWorker.exitAndDeinit()` ## Tests 20 tests in `test/js/bun/cron/in-process-cron.test.ts`: validation, stop/ref/unref/Disposable, process-alive semantics, real minute-boundary firing, async-callback scheduling, `uncaughtException`/`unhandledRejection` paths, and `--hot` ghost-job clearing.
Fixes #7004
Adds an in-process overload to
Bun.cronthat runs a callback on the event loop at cron-scheduled intervals — similar to Deno.cron.This complements the existing OS-level
Bun.cron(path, schedule, title)(crontab/launchd/schtasks) with a lighter option that works in containers and needs no system cron daemon. Dispatched ontypeof args[1] === "function".Behavior
setTimeout— a synchronous throw emitsuncaughtException; a rejected Promise emitsunhandledRejection. Without a listener the process exits with code 1. The job reschedules itself after an error.--hotsafe — all in-process cron jobs are cleared before the module graph re-evaluates, so editing the schedule, editing the callback, or deleting theBun.cron(...)line entirely all take effect on save without leaking timers. Worker termination also clears that worker's jobs.Disposable—using job = Bun.cron(...)auto-stops at scope exit..ref()(default) keeps the process alive;.unref()lets it exit.Implementation
CronJobZig struct backed by a.classes.ts-generated JS wrapperCronExpression.next()from the OS-level cron to compute wall-clock fire times; converts to monotonic delta for theEventLoopTimerheapJSValue.then()with handlers registered in thePromiseFunctionstable;pending_promiseis held sounhandledRejectionreceives the actual promisebun.ptr.RefCount+jsc.JSRefkeep the wrapper alive across the pending-promise windowjobs_listis scanned byclearAllForVM(vm)fromVirtualMachine.reload()andWebWorker.exitAndDeinit()Tests
20 tests in
test/js/bun/cron/in-process-cron.test.ts: validation, stop/ref/unref/Disposable, process-alive semantics, real minute-boundary firing, async-callback scheduling,uncaughtException/unhandledRejectionpaths, and--hotghost-job clearing.