Files
familysync/.planning/quick/260610-i4x-replace-node-cron-with-setinterval-in-ba/260610-i4x-PLAN.md
T

8.6 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
quick-260610-i4x 01 execute 1
apps/api/src/broker/poller.ts
apps/api/src/broker/outboxWorker.ts
apps/api/src/broker/reminderScheduler.ts
true
QUICK-i4x
truths artifacts key_links
The three background workers schedule their callbacks with setInterval, not node-cron schedule()
No worker file imports node-cron
runPoll / runOutboxDrain / runReminderCheck callback bodies are unchanged (still .catch-wrapped)
Interval timings are preserved: poller 5 min, outbox 15 s, reminder 1 min
apps/api typechecks clean and the broker unit tests still pass
path provides contains
apps/api/src/broker/poller.ts startBrokerPoller scheduling runPoll via setInterval(5min) setInterval
path provides contains
apps/api/src/broker/outboxWorker.ts startOutboxWorker scheduling runOutboxDrain via setInterval(15s) setInterval
path provides contains
apps/api/src/broker/reminderScheduler.ts startReminderScheduler scheduling runReminderCheck via setInterval(1min) setInterval
from to via pattern
apps/api/src/broker/poller.ts runPoll setInterval(cb, 5 * 60 * 1000) setInterval(
from to via pattern
apps/api/src/broker/outboxWorker.ts runOutboxDrain setInterval(cb, 15 * 1000) setInterval(
from to via pattern
apps/api/src/broker/reminderScheduler.ts runReminderCheck setInterval(cb, 60 * 1000) setInterval(
Replace the node-cron `schedule()` scheduling mechanism with `setInterval` in the three background worker `start*` functions so scheduled tasks actually fire in the long-running API process.

Purpose: node-cron 4.2.1 silently skips scheduled executions in the long-lived server process — the orchestrator proved this live against the running prod container (continuous [NODE-CRON][WARN] missed execution at every tick while CPU sat at 0.15%; a plain setInterval(…,1000) in the SAME container fired 8/8 ticks, and a FRESH node-cron process fired 8/8 — only the long-lived process misbehaves). Net effect: the CalDAV poller, the outbox drain, and the reminder scan never fire on schedule. They only ran when triggered manually during debugging.

Output: Three worker files where startBrokerPoller, startOutboxWorker, and startReminderScheduler schedule their existing callbacks with setInterval at the same intervals, with the now-unused node-cron import removed and stale doc comments updated.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md </execution_context>

@./CLAUDE.md

The three worker files — only the start* functions and their doc comments change.

Do NOT modify runPoll / runOutboxDrain / runReminderCheck logic.

@apps/api/src/broker/poller.ts @apps/api/src/broker/outboxWorker.ts @apps/api/src/broker/reminderScheduler.ts

Task 1: Swap node-cron schedule() for setInterval in the three worker start* functions apps/api/src/broker/poller.ts, apps/api/src/broker/outboxWorker.ts, apps/api/src/broker/reminderScheduler.ts In each of the three files, replace the node-cron scheduling call inside the start* function with a setInterval call, keeping the callback body EXACTLY as written (it already wraps the run function in `.catch(...)`). Then remove the now-unused `import { schedule } from 'node-cron'` line from each file. Do NOT touch runPoll / runOutboxDrain / runReminderCheck, the constants, helpers, or any other code. Do NOT touch index.ts, package.json, or the lockfile — the start* function names and signatures are unchanged so index.ts keeps working as-is.

Exact mapping (interval in ms = the cron cadence; setInterval fires after the first interval, same first-run timing as the old cron — do NOT add an immediate kick, that is out of scope):

  • poller.ts startBrokerPoller: change schedule('*/5 * * * *', () => { ... }) to setInterval(() => { ... }, 5 * 60 * 1000) — keep the same arrow-function callback body (the runPoll().catch(...) block).

  • outboxWorker.ts startOutboxWorker: change schedule('*/15 * * * * *', () => { ... }) to setInterval(() => { ... }, 15 * 1000) — keep the same runOutboxDrain().catch(...) body.

  • reminderScheduler.ts startReminderScheduler: change schedule('* * * * *', () => { ... }) to setInterval(() => { ... }, 60 * 1000) — keep the same runReminderCheck().catch(...) body.

Do NOT call .unref() on the returned Timer — we WANT the interval to keep the event loop alive (same effect as cron kept the process alive). Storing the returned Timer handle in a module-level variable is acceptable if cleaner, but not required — fire-and-forget matches the current pattern (the workers run for the process lifetime).

Update the now-stale doc comments that reference node-cron so they describe setInterval, keeping the WHY. Specifically:

  • poller.ts: the file-header line "runs every 5 minutes via node-cron", the "startBrokerPoller wraps it in node-cron's 5-minute schedule." line, the "Source: https://github.com/node-cron/node-cron ..." source line, and the "Starts the 5-minute background polling schedule." block.
  • outboxWorker.ts: "startOutboxWorker wraps it in a 15-second node-cron schedule." and the "Source: https://github.com/node-cron/node-cron (v4 stable)" line, plus the "Starts the 15-second background outbox drain schedule." block.
  • reminderScheduler.ts: the file-header "Fires every minute via node-cron." line and the "Start the 1-minute reminder scan schedule." block comment ("keeps the cron out of the test process" → setInterval phrasing). Each updated comment must briefly state the WHY (node-cron 4.2.1 skipped scheduled executions in the long-running server process, so scheduling uses setInterval instead). Keep edits brief — do not rewrite the surrounding decision/threat-mitigation prose. Leave the node-cron dependency in package.json (now unused, harmless); it is removable later but removing it now risks lockfile drift and is out of scope for this tight change. cd /home/luc/Projects/familysync && ! grep -rn "node-cron" apps/api/src/broker/poller.ts apps/api/src/broker/outboxWorker.ts apps/api/src/broker/reminderScheduler.ts | grep -v '^\s*[0-9]:.//' ; grep -c "setInterval" apps/api/src/broker/poller.ts apps/api/src/broker/outboxWorker.ts apps/api/src/broker/reminderScheduler.ts cd /home/luc/Projects/familysync && pnpm --filter @familysync/api typecheck cd /home/luc/Projects/familysync && pnpm --filter @familysync/api exec vitest run tests/broker/ All three files import setInterval-based scheduling with no remaining import { schedule } from 'node-cron'; each start* function calls setInterval at the correct interval (poller 300000 ms, outbox 15000 ms, reminder 60000 ms) with its original callback body intact; pnpm --filter @familysync/api typecheck exits 0; and pnpm --filter @familysync/api exec vitest run tests/broker/ reports all broker tests green (runPoll / runOutboxDrain / runReminderCheck behavior unchanged).
- No `import { schedule } from 'node-cron'` remains in any of the three worker files. - `grep setInterval` matches in all three files (one per start* function). - runPoll / runOutboxDrain / runReminderCheck logic is byte-for-byte unchanged (only doc comments and the scheduling call were touched). - `pnpm --filter @familysync/api typecheck` exits 0. - `pnpm --filter @familysync/api exec vitest run tests/broker/` passes (crypto, expand, outboxWorker, poller, reminderScheduler, sync, vevent, write). Do NOT run the full suite — it has ~68 pre-existing DB-integration failures unrelated to this change (MariaDB host port not exposed in prod-compose). - index.ts, package.json, and the lockfile are untouched.

<success_criteria> The three background workers schedule their callbacks via setInterval at the original intervals (5 min / 15 s / 1 min), node-cron is no longer imported in src/broker, the apps/api typecheck is clean, and the broker unit tests still pass — so in the long-running API process the poller, outbox drain, and reminder scan will now actually fire on schedule. </success_criteria>

Create `.planning/quick/260610-i4x-replace-node-cron-with-setinterval-in-ba/260610-i4x-SUMMARY.md` when done.