Skip to content

fix(core): auto-publish scheduled content and fix SQLite datetime comparison#1216

Open
scottbuscemi wants to merge 1 commit into
mainfrom
fix/scheduled-publishing-917
Open

fix(core): auto-publish scheduled content and fix SQLite datetime comparison#1216
scottbuscemi wants to merge 1 commit into
mainfrom
fix/scheduled-publishing-917

Conversation

@scottbuscemi
Copy link
Copy Markdown
Contributor

@scottbuscemi scottbuscemi commented May 29, 2026

What does this PR do?

Fixes scheduled posts never becoming published. Three independent bugs:

  1. No auto-publish mechanism -- ContentRepository.findReadyToPublish() existed but was never called outside tests (dead code). The system relied on a read-time workaround in buildStatusCondition() that treated past-due scheduled content as "effectively published" at query time, but the database status never transitioned from scheduled to published.

  2. SQLite format mismatch -- scheduled_at is stored as ISO 8601 with T and Z (e.g. 2026-05-05T01:41:59.000Z) but SQLite's datetime('now') returns 2026-05-05 01:43:15. On same-day comparisons, T (0x54) > space (0x20), so scheduled_at <= datetime('now') was always false on SQLite/D1.

  3. Wrong execution path for the publish check -- The initial approach wired publishScheduledContent into the cron system's systemCleanup callback. This failed for two reasons:

    • On Node, the NodeCronScheduler only fires based on plugin cron tasks. With no plugins registered, the timer polls at MAX_INTERVAL_MS (5 minutes) -- far too slow for timely publishing.
    • On Workers, the PiggybackScheduler fires promises without waitUntil(), so Cloudflare can kill the DB queries before they complete after the response is sent.

Closes #917

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4 via OpenCode

Screenshots / test output

Changes

middleware.ts -- Added publishScheduledContent call directly in the doInit() middleware path using after() with 15-second debounce. after() properly extends Worker lifetime via waitUntil on Cloudflare, so the DB queries survive past the response. Captures runtime.db (ALS-aware) before entering after() so playground/DO sessions get the correct database.

cleanup.ts -- Added publishScheduledContent() that iterates all collections, finds items where scheduled_at <= now, and publishes each via ContentRepository.publish() (revision promotion, data sync, schedule clearing, atomic per-item).

emdash-runtime.ts -- Also wired publishScheduledContent into the cron scheduler tick as a backup path (runs alongside runSystemCleanup). Called tickCron() from middleware so the piggyback scheduler fires on Workers.

loader.ts -- Fixed buildStatusCondition() to wrap scheduled_at in datetime() on SQLite, normalizing ISO 8601 T/Z format for comparison with datetime('now').

content.ts -- Fixed findReadyToPublish() to use DB-side datetime() comparison instead of JS toISOString() for cross-dialect correctness.

snapshot.ts -- Replaced hardcoded datetime('now') with dialect-aware currentTimestampValue(db) and added datetime() wrapping for SQLite.

Verified end-to-end

Scheduled posts auto-publish within 15 seconds of their scheduled time on the next request. Tested on local Node dev server by:

  1. Creating a draft post
  2. Scheduling it 5 seconds in the future
  3. Waiting for the time to pass
  4. Making an authenticated request
  5. Confirming the post status flipped from scheduled to published

Test output

 Test Files  221 passed (221)
      Tests  3493 passed (3493)

11 new tests:

Relation to PR #772

PR #772 adds an external POST /_emdash/api/cron/publish-due endpoint. This PR is complementary -- it makes scheduling work out of the box without requiring external cron configuration.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: 57f7a6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 29, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1216

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1216

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1216

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1216

emdash

npm i https://pkg.pr.new/emdash@1216

create-emdash

npm i https://pkg.pr.new/create-emdash@1216

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1216

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1216

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1216

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1216

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1216

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1216

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1216

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1216

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1216

commit: 57f7a6c

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 57f7a6c May 29 2026, 08:41 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 57f7a6c May 29 2026, 08:41 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 57f7a6c May 29 2026, 08:40 PM

@scottbuscemi scottbuscemi force-pushed the fix/scheduled-publishing-917 branch from bf8f00b to 5fc2f12 Compare May 29, 2026 20:05
…parison

Two independent bugs prevented scheduled posts from ever becoming published:

1. No auto-publish mechanism existed — findReadyToPublish() was dead code,
   never called outside tests. Added publishScheduledContent() and wired it
   into middleware via after() with 15 s debounce. Uses after() to extend
   Worker lifetime via waitUntil on Cloudflare. Also kept as a backup in
   the cron system cleanup tick.

2. SQLite format mismatch (#917) — scheduled_at stores ISO 8601 with T/Z
   separators but datetime('now') returns space-separated format. The
   lexicographic comparison always returned false. Fixed by wrapping both
   sides in datetime() on SQLite in loader.ts, content.ts, and snapshot.ts.

Verified end-to-end: scheduled posts auto-publish within 15 seconds of
their scheduled time on the next request.

Fixes #917
@scottbuscemi scottbuscemi force-pushed the fix/scheduled-publishing-917 branch from 5fc2f12 to 57f7a6c Compare May 29, 2026 20:38
@github-actions
Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scheduled posts never become visible on SQLite/D1: string comparison between ISO scheduled_at and SQLite CURRENT_TIMESTAMP always evaluates false

1 participant