Conversation
🦋 Changeset detectedLatest commit: e0cb2ad The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
f09fdf0 to
8b06127
Compare
8b06127 to
c5ce6e1
Compare
c5ce6e1 to
0f8952d
Compare
@cloudflare/autoconfig
@cloudflare/build-output-utils
@cloudflare/codemods
@cloudflare/config
@cloudflare/containers-shared
create-cloudflare
@cloudflare/deploy-helpers
@cloudflare/kv-asset-handler
miniflare
@cloudflare/pages-functions
@cloudflare/pages-shared
@cloudflare/unenv-preset
@cloudflare/vite-plugin
@cloudflare/vitest-plugin
@cloudflare/workers-auth
@cloudflare/workers-editor-shared
@cloudflare/workers-utils
wrangler
commit: |
0f8952d to
d771b20
Compare
d771b20 to
d99127b
Compare
| const epochMs = date.getTime(); | ||
| return { kind: "exact", epochMs, utc: date.toISOString() }; |
There was a problem hiding this comment.
🟡 Out-of-range calendar times remain triggerable
resolveUtcCalendarTime accepts years outside the scheduled API’s epoch bounds. These dates enable Trigger, but every invocation fails with status 400.
Learn more
Calendar values and epoch values reach the same scheduled_time request field. The epoch editor enforces MIN_DATE_EPOCH_MS and MAX_DATE_EPOCH_MS, but the calendar parser only checks JavaScript's broader year range. The Trigger gate treats every exact calendar result as valid. The API then rejects values outside its integer bounds.
Example: 9999-12-31T23:59:59.999 resolves successfully and enables Trigger. Its epoch exceeds MAX_DATE_EPOCH_MS, so the scheduled endpoint returns 400 instead of invoking the Worker.
Recommended fix: Return an invalid calendar resolution when date.getTime() falls outside MIN_DATE_EPOCH_MS through MAX_DATE_EPOCH_MS. Update the boundary tests to cover calendar values immediately inside and outside that range.
| const epochMs = date.getTime(); | |
| return { kind: "exact", epochMs, utc: date.toISOString() }; | |
| const epochMs = date.getTime(); | |
| if (epochMs < MIN_DATE_EPOCH_MS || epochMs > MAX_DATE_EPOCH_MS) { | |
| return { | |
| kind: "invalid", | |
| error: "Date and time are outside the supported scheduled-time range.", | |
| }; | |
| } | |
| return { kind: "exact", epochMs, utc: date.toISOString() }; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| let duplicateId: string | undefined; | ||
| setWorkers((current) => { | ||
| const entry = current[workerName] ?? emptyState(); | ||
| const row = entry.rows.find((candidate) => candidate.id === id); | ||
| if (!row) { | ||
| return current; | ||
| } | ||
| const duplicate = duplicateCronRow(row); | ||
| duplicateId = duplicate.id; | ||
| return { | ||
| ...current, | ||
| [workerName]: { ...entry, rows: [...entry.rows, duplicate] }, | ||
| }; | ||
| }); | ||
| return duplicateId; |
There was a problem hiding this comment.
🟡 Duplicated cron rows miss focus
duplicateRow returns before its state updater assigns duplicateId. The caller receives undefined, so the new row never receives requested focus.
Learn more
React queues functional state updaters and can execute them after the event handler returns. Assigning duplicateId inside that updater cannot provide a synchronous return value. The caller immediately passes the returned value to focusSoon, which takes its fallback path when the value is undefined.
Example: Duplicating 0 17 * * sun adds the draft row, but focus moves to the Add draft control or pane heading instead of its Cron expression input.
Recommended fix: Create the duplicate and its ID before calling setWorkers, or change duplicateRow to accept a focus callback/effect that runs after the row is committed. Keep row lookup inside the state update so concurrent changes cannot duplicate a removed row.
Was this helpful? React with 👍 or 👎 to provide feedback.
| /** Persist only editable custom-row drafts; transient and configured state is omitted. */ | ||
| export function writePersistedCustomCronRows( | ||
| storage: Storage, | ||
| key: string, | ||
| rows: CronRow[] | ||
| ): void { | ||
| const customRows = rows | ||
| .filter((row) => row.source === "custom") | ||
| .slice(0, MAX_PERSISTED_CUSTOM_ROWS) | ||
| .map(persistedDraft); | ||
| if (customRows.length === 0) { | ||
| remove(storage, key); | ||
| return; | ||
| } | ||
| const raw = JSON.stringify(customRows); | ||
| if (byteLength(raw) > MAX_PERSISTED_CUSTOM_ROWS_BYTES) { | ||
| remove(storage, key); | ||
| return; | ||
| } | ||
| try { | ||
| storage.setItem(key, raw); | ||
| } catch { | ||
| // Quota, privacy, and security errors must not break the editor. | ||
| } |
| const cronValid = | ||
| row.cron.trim() !== "" && | ||
| (row.cronInputMode !== "builder" || | ||
| (row.builderApplied === true && builder.expression === row.cron)); |
There was a problem hiding this comment.
| case "last-day-of-month": | ||
| expression = `${minute} ${hour} L * *`; | ||
| break; | ||
| case "last-weekday-of-month": | ||
| expression = `${minute} ${hour} LW * *`; | ||
| break; | ||
| case "nearest-weekday": { | ||
| const day = field( | ||
| errors, | ||
| "dayOfMonth", | ||
| draft.dayOfMonth, | ||
| 1, | ||
| 31, | ||
| "Day of month" | ||
| ); | ||
| expression = `${minute} ${hour} ${day}W * *`; | ||
| break; | ||
| } | ||
| case "last-named-weekday": | ||
| expression = `${minute} ${hour} * * ${draft.weekday}L`; | ||
| break; | ||
| case "nth-weekday": { | ||
| const occurrence = field( | ||
| errors, | ||
| "occurrence", | ||
| draft.occurrence, | ||
| 1, | ||
| 5, | ||
| "Weekday occurrence" | ||
| ); | ||
| expression = `${minute} ${hour} * * ${draft.weekday}#${occurrence}`; | ||
| break; |
| const activeWorkerName = loaderData.bootstrapAuthoritative | ||
| ? selectedWorker?.name | ||
| : search.worker; |
There was a problem hiding this comment.
🟡 Successful refresh leaves explorer unavailable
After bootstrap fails, activeWorkerName remains the query value because bootstrapAuthoritative never changes. Successful refreshes cannot canonicalize workers or repopulate the sidebar.
Learn more
The root loader owns the Worker list and bootstrapAuthoritative, while CronTriggersProvider.refresh updates only provider-local state. A failed initial request therefore freezes the route and sidebar in bootstrap-failure mode. The provider can fetch valid metadata later, but this route still selects the original query Worker and the root layout still sees no Workers.
Example: Open ?worker=missing-worker while the initial Worker request returns 500. The automatic refresh then returns worker-1. The page keeps reading missing-worker, shows the unavailable state, and offers no selector, although worker-1 is now available.
Recommended fix: Make a successful refresh update or invalidate the root Worker loader, then derive and canonicalize activeWorkerName from the refreshed authoritative list. Preserve the requested query only while metadata remains unavailable.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const poll = window.setInterval(() => { | ||
| if (document.visibilityState === "visible") { | ||
| void refresh(refreshWorker, true); | ||
| } |
There was a problem hiding this comment.
🟡 Slow polling discards every refresh
When refresh exceeds five seconds, the poll starts another request and invalidates the first generation. Sustained slow responses never update Worker metadata.
Learn more
Every poll starts a refresh without checking refreshingGeneration. RefreshGenerationTracker then marks only the newest request as current, and completed older requests exit without applying metadata. If each request takes longer than the polling interval, a newer request always exists before the previous response arrives.
Example: The Worker endpoint consistently responds in six seconds. Requests start at 0, 5, 10, and 15 seconds. Each response arrives one second after its successor starts, fails isLatest, and gets discarded.
Recommended fix: Do not start an automatic refresh while that Worker's previous refresh is pending. Alternatively, schedule the next poll after the current refresh settles instead of using an overlapping interval.
Was this helpful? React with 👍 or 👎 to provide feedback.
| import { Route as EmailRouteImport } from './routes/email' | ||
| import { Route as IndexRouteImport } from './routes/index' | ||
| import { Route as ObservabilityIndexRouteImport } from './routes/observability/index' | ||
| import { Route as CronTriggersIndexRouteImport } from './routes/cron-triggers/index' |
Adds a Cron Trigger group to the local explorer | Internal SPEC
Provides interface for users to test configured crons, and create custom crons ad-hoc for quick testing without having to reconfigure the worker every time a user wants to experiment with a new cron.
Two lists are provided,
Configured Crons(left pane) andCustom Crons(right pane). AConfigured Cronis one which is defined by the user in the Worker's config file. ACustom Cronis a transient cron defined and manipulated from the Local Explorer UI.All crons:
Configured Crons:triggers.cronsconfig option.Custom Crons:Note
All images in this description come from a WIP UI version.
Example landing page, showing the
Configured CronsandCustom Cronspanes with one cron in eachCron expression builder for the 7:35AM on the last weekday of each month. The expression stays up to date as the builder is adjusted.
Examples of both the epoch millisecond and data and time setters, used to dictate the
scheduledTimepassed to thescheduled()handler.