[WSAPI-8] Add rolling-hour rate ceiling + 401/429 cooldown #10
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/WSAPI-8"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Ticket
WSAPI-8 Add a rolling-hour rate ceiling + 401/429 cooldown to
/sync/triggerSummary
WS_MAX_SYNCS_PER_HOUR, default 12) that bounds the on-demandPOST /sync/triggerpath (immediate + trailing debounced runs) — the nightly cron is deliberately exempt from the check, since it only fires once a day and could never spend a 12/hour budget in practice.syncStatusdocument. Unlike the ceiling, the cooldown gates both the on-demand trigger and the nightly cron — it's a "Wealthsimple said stop" signal that protects the account regardless of which path is asking.GET /statusgains arateLimit: { maxSyncsPerHour, syncsRemainingThisHour }field, andjobs.activityFeednow also carriescooldownUntil/cooldownReason.POST /sync/triggerwidens itsreasonfield todebounced | rateLimited | cooldown— all three are still200, not failures.server/lib/wealthsimple/sync,WS_MAX_SYNCS_PER_HOUR/COOLDOWN_STATUSES) adapted to this repo's simpler, mutex-free, per-job-status model — this closes the gap BTAPI-76's own pre-PR review flagged: without a ceiling, a stuck or repeated doorbell forwarder could drive far more than the 12/hour design budget; without a cooldown, a Wealthsimple refusal would just be retried at normal cadence, risking a locked account on a live credit card.sync/activity-feed/index.ts's own header comment.Verification
bun run type-check,bun run lint,bun run format:check— all clean.bun test— 384 passed, 0 failed, across 37 files.🤖 Generated with Claude Code
HIGH: - sync/activity-feed: the trailing run's async IIFE now wraps attemptSync() in a try/catch. Its cooldown check (SyncStatus.getCooldown) is an unguarded DB read that ran ahead of performSync()'s own double try/catch, so a Mongo rejection there was an unhandled rejection that could crash the process. MEDIUM: - sync/activity-feed: the rolling-hour ceiling charge moved from performSync() (unconditional) into attemptSync() (the on-demand chokepoint), so the nightly cron — which calls performSync() directly and is deliberately ceiling-exempt — no longer spends on-demand budget it was never charged against by contract. - Exported cooldownReasonFor() as a direct, unit-testable seam for the 401/429 COOLDOWN_STATUSES mapping (401->rejected, 429->rateLimited, and the undefined cases), replacing a comment that claimed coverage which didn't actually exist. - Added cooldown-field assertions to the existing non-cooldown failure tests (a 400, and a session-halted run) proving they leave cooldownUntil/ cooldownReason alone. - lib/sync-status: extracted a named, readonly Cooldown interface used everywhere the { until, reason } pair traveled as an unnamed inline type. LOW: - lib/sync-status: widened cooldownUntil's document type to Date | string | null, matching the runtime defensive branch that already handles a raw string. - sync/activity-feed: dropped the redundant CooldownReason import (now referenced as SyncStatus.CooldownReason), wrapped COOLDOWN_STATUSES in Object.freeze for runtime parity with budget-tracker-api's reference, and had SyncTriggerResult.reason reference the SyncDecline type instead of repeating its members. - routes/status test now resets the rate-ceiling state per test so the rateLimit assertion is exact rather than bounds-only; added a ceiling- recovery assertion that a trigger actually succeeds again, and a cron test proving it resumes once an expired cooldown clears. - lib/sync-status test: collapsed two cooldown assertions each into one full-object toEqual, matching this file's existing no-partial-match style. - routes/sync: added two route-level tests exercising a rate-limited and a cooldown decline through the real POST /sync/trigger endpoint (both 200), and widened the route's and CLAUDE.md's docs to describe the three-valued decline reason and the new config/status surface. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>Tightened the sync/activity-feed/ and lib/config/ architecture-tree entries this ticket added to — folded the WSAPI-8 addition into the existing sentence and traded verbose verbs ("bounds ... specifically", "gates both ... and the nightly cron") for parentheticals, net one line shorter with no loss of information. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>