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

165 lines
8.6 KiB
Markdown

---
phase: quick-260610-i4x
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- apps/api/src/broker/poller.ts
- apps/api/src/broker/outboxWorker.ts
- apps/api/src/broker/reminderScheduler.ts
autonomous: true
requirements:
- QUICK-i4x
must_haves:
truths:
- "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"
artifacts:
- path: "apps/api/src/broker/poller.ts"
provides: "startBrokerPoller scheduling runPoll via setInterval(5min)"
contains: "setInterval"
- path: "apps/api/src/broker/outboxWorker.ts"
provides: "startOutboxWorker scheduling runOutboxDrain via setInterval(15s)"
contains: "setInterval"
- path: "apps/api/src/broker/reminderScheduler.ts"
provides: "startReminderScheduler scheduling runReminderCheck via setInterval(1min)"
contains: "setInterval"
key_links:
- from: "apps/api/src/broker/poller.ts"
to: "runPoll"
via: "setInterval(cb, 5 * 60 * 1000)"
pattern: "setInterval\\("
- from: "apps/api/src/broker/outboxWorker.ts"
to: "runOutboxDrain"
via: "setInterval(cb, 15 * 1000)"
pattern: "setInterval\\("
- from: "apps/api/src/broker/reminderScheduler.ts"
to: "runReminderCheck"
via: "setInterval(cb, 60 * 1000)"
pattern: "setInterval\\("
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
</execution_context>
<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
</context>
<tasks>
<task type="auto">
<name>Task 1: Swap node-cron schedule() for setInterval in the three worker start* functions</name>
<files>apps/api/src/broker/poller.ts, apps/api/src/broker/outboxWorker.ts, apps/api/src/broker/reminderScheduler.ts</files>
<action>
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.
</action>
<verify>
<automated>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</automated>
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api typecheck</automated>
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api exec vitest run tests/broker/</automated>
</verify>
<done>
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).
</done>
</task>
</tasks>
<verification>
- 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.
</verification>
<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>
<output>
Create `.planning/quick/260610-i4x-replace-node-cron-with-setinterval-in-ba/260610-i4x-SUMMARY.md` when done.
</output>