feat/lc-62-scheduled-send #156

Merged
longjacksonle merged 2 commits from feat/lc-62-scheduled-send into main 2026-05-20 05:10:13 +02:00
No description provided.
Groundwork for the scheduled-send dispatcher to share the post-insert side
effects (broadcast, mention reconcile, push, DM implicit-mention) with the
live POST handler. No behavior change. No test changes (the load-bearing
verification that the extraction is equivalent).

The new pub(crate) fn lives next to post_message in routes/room.rs so the
local helpers (author_label, build_snippet, fanout_mention_events,
resolve_tokens_for_room) stay in scope without re-exports.
feat(scheduled): schema + db module + orphan-sweep guard (LC-62)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 27m20s
7c4d808a8e
Migration 0033 adds scheduled_messages with delivered_at + failed_reason as the state-derivation columns (pending / delivered / dropped derived inline at every query site, no enum column to drift against the SQL) and three partial indexes: idx_sched_due for the dispatcher tick, idx_sched_user for the per-user pending list, idx_sched_file for the orphan-sweep NOT EXISTS lookup. room_id cascades on room delete; parent_id / quote_id / file_id are SET NULL so a deleted parent message or vanished upload degrades the delivery to top-level / unquoted / text-only instead of failing. No FK on user_id (auth lives in a separate pool); routes/account.rs::purge_user_chat will pick up the user-purge in Task 6 via delete_for_user.

db::scheduled exposes the surface Tasks 3 and 4 consume: insert_scheduled / get_scheduled / list_pending_for_user / list_recent_dropped_for_user for the management UI, update_scheduled / delete_scheduled with user-scoped WHERE clauses, delete_for_user that takes &mut SqliteConnection for the account-purge transaction, select_due / try_claim for the dispatcher's atomic claim, mark_dropped for in-transaction validation refusals, bump_attempt for post-rollback exponential backoff. try_claim's UPDATE-with-WHERE-delivered_at-IS-NULL is the load-bearing atomicity primitive; on rows_affected == 0 the caller ROLLBACKs and moves on.

db::uploads::select_orphans_older_than gains a NOT EXISTS clause against scheduled_messages so a scheduled-for-tomorrow attachment is not GC'd before delivery. Cancelled scheduled rows correctly stop protecting the upload (row gone -> upload becomes a normal orphan -> next sweep removes); delivered scheduled rows set delivered_at AND link the upload's message_id atomically inside the dispatch transaction. idx_sched_file keeps the NOT EXISTS lookup O(1) per orphan candidate.

Migration-drift sweep per CLAUDE.md drift category 2: 0033 appended to nine hand-rolled setup_chat_pool helpers that already include 0012_uploads (and therefore the full FK chain through file_uploads). Five curated-subset test files (db_custom_emojis, db_enclave, db_notifications, scale_mentions, db_private_rooms) intentionally omit 0033 because they also omit 0012_uploads, and SQLite's FK cascade processor trips over the SET NULL action on a missing parent table when an enclave-cascade chain crosses messages. Those tests never reference scheduled_messages, so the omission is safe; documented in this commit so the next migration author sees the precedent.

tests/db_scheduled.rs covers the CRUD round-trip, ownership scoping on edit / cancel, select_due filtering across past-due / future / cooldown / delivered rows, try_claim atomicity (first call wins, second returns false), mark_dropped inside a BEGIN IMMEDIATE transaction after try_claim, bump_attempt incrementing attempt_count and excluding the row from select_due, list ordering / state filtering / LIMIT, delete_for_user purging all states for one user without touching another, and room-delete cascade. tests/uploads_sweep.rs gains orphan_referenced_by_pending_scheduled_message_is_protected: an orphan past the 24h threshold survives the sweep while a pending scheduled row references it, and becomes eligible the moment the scheduled row is cancelled.

Resting point: scheduled_messages exists, the orphan sweep knows to skip protected uploads, nothing else uses any of it yet. Task 3 (spawn_scheduled_dispatcher), Task 4 (routes), Task 5 (composer modal), and Task 6 (purge_user_chat cross-pool delete) build on this in PR-B; if PR-B is delayed, PR-A is harmless on its own.
longjacksonle deleted branch feat/lc-62-scheduled-send 2026-05-20 05:10:13 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!156
No description provided.