feat(email,scheduler,offline): idempotent sends, template validation, cron validation, connectivity debounce - #1289
Merged
RUKAYAT-CODER merged 1 commit intoAug 30, 2026
Conversation
… cron validation, connectivity debounce Closes rinafcode#1171 Closes rinafcode#1177 Closes rinafcode#1178 Closes rinafcode#1181 rinafcode#1171 — Debounce offline-mode connectivity flapping. createConnectivityDebouncer reports a state only once it has held for the window, and a value returning to the last settled one cancels the pending timer rather than restarting it: offline to online and back inside the window is not a change and should fire nothing. useOfflineMode drives its listeners through it and syncs only on a settled online state, so a wifi handover no longer starts a sync that the next event interrupts. rinafcode#1177 — Deduplicate transactional emails by idempotency key. EmailQueue registers the key before the send starts, so concurrent duplicates dedupe as well as sequential ones, and a repeated enqueue returns the first send's result instead of delivering twice. A failed send releases its key, since a failure is not a delivery and must not block the retry. Keys expire on a TTL, with pruning and clearing exposed. deriveIdempotencyKey derives one from message content for callers that have nothing better, normalising recipient order and case. rinafcode#1178 — Validate email template variables at render time. The required variables are read from the templates themselves rather than a hand-maintained list, so the schema cannot drift from what the templates use. getTemplate now throws MissingTemplateVariablesError instead of rendering an unresolved placeholder as an empty string; an empty or whitespace value counts as missing, while zero and false do not. Unused payload keys are reported too, since `userName` for a template wanting `name` is a better clue than a blank space in the sent mail. allowMissing keeps previews and tests working. rinafcode#1181 — Validate cron expressions in the export scheduler. validateCron returns every field-level reason an expression was rejected rather than a bare boolean, so a form can say which field is wrong. Parsing is now strict: parseInt stopped at the first non-digit and accepted "5x" as 5. 7 is accepted as Sunday, as crontab does. The scheduler validates before enqueuing, and each schedule is isolated in the sweep — one malformed expression used to throw out of getNextRunTime and abort the loop, so a single bad string stopped every other schedule from running. Also fixes a delivery bug found while adding dedupe: every enqueue shared one resolver, so with two messages in flight an enqueue could resolve with an unrelated message's result. 123 tests pass across the touched files.
|
@ThatCodeBabe Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Contributor
|
Thank you for contributing to the project. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four independent hardening fixes: two in transactional email, one in the export scheduler, one in offline mode.
Closes #1171
Closes #1177
Closes #1178
Closes #1181
#1171 — Debounce offline-mode connectivity flapping
src/utils/pwaUtils.ts,src/hooks/useOfflineMode.tsxcreateConnectivityDebouncerreports a connectivity state only once it has held for the window. Two properties beyond plain debouncing matter here:onlineevents on an already-online connection stays silent.useOfflineModedrives itsonline/offlinelisteners through it and syncs only on a settled online state, so a train tunnel or a wifi handover no longer starts a sync that the next event interrupts — which is how the queue ended up never draining while burning a retry on each attempt. The hook now also exposesisOnlineandflushConnectivity, and cancels any pending transition on unmount so a timer cannot fire against a torn-down service.#1177 — Deduplicate transactional emails by idempotency key
src/lib/email/queue.ts,src/lib/email/index.ts, newsrc/lib/email/idempotency.tsEmailQueue.enqueueaccepts anidempotencyKey; a second enqueue under the same key returns the first send's result rather than delivering again. Three details make it actually hold:pruneIdempotencyCacheandclearIdempotencyCache, so the map is not an unbounded leak in a long-lived process.deriveIdempotencyKeyderives a key from message content for callers with nothing better to hand, normalising recipient order and case — those are not part of what the message is. Tags andreplyToare excluded for the same reason.#1178 — Validate email template variables at render time
src/lib/email/templates.ts,src/lib/email/types.tsRequired variables are extracted from the templates themselves, not a hand-maintained list, so a declared schema cannot drift away from what the template actually references.
getTemplatenow throwsMissingTemplateVariablesErrorrather than rendering an unresolved placeholder as an empty string — a password-reset email whose link silently renders as nothing is worse than one never sent, because the user cannot tell it is broken.falsecount as supplied —expiresInMinutes: 0is a value, not an omission.userNamepassed to a template that wantsnameproduces one missing and one unused entry, which is a far better clue than a blank space in the delivered mail.allowMissingkeeps previews and tests able to render an incomplete payload.The one existing caller (
hybridEvents) supplies all five variables of the verification template, so nothing in the app starts throwing.#1181 — Validate cron expressions in the export scheduler
src/lib/export-scheduler/cron-parser.ts,src/lib/export-scheduler/scheduler-service.tsvalidateCronreturns every field-level reason an expression was rejected — field, offending value, and why — instead of a bare boolean, so a form can tell the user that0 0 * * 9has adayOfWeekoutside 0-7. Every field is checked rather than stopping at the first, so one save surfaces every mistake.Parsing is now strict.
parseIntstops at the first non-digit, so the previous validator read"5x"as5and accepted0 0 5x * *— an expression cron itself rejects.7is accepted as a second spelling of Sunday, as crontab does.On the service side,
validateScheduleexposes the check for the save path, andqueueExportJobvalidates before enqueuing: queuing first and then failing to compute the next run leaves the schedule due forever, re-running the same export on every sweep. Each schedule is also isolated insidecheckDueSchedules— a malformed expression previously threw out ofgetNextRunTimeand aborted the loop, so one bad string stopped every other schedule from running too.A delivery bug found while adding dedupe
EmailQueue.processtook theresolveof a singleenqueuecall and used it for whatever job it dequeued. WithmaxConcurrent: 2and two messages in flight, an enqueue could resolve with an unrelated message's result. Each job now carries its own resolver.Verification
Three suites fail on
mainfor reasons unrelated to this branch and are untouched here:services/offlineSync.test.ts(the session gate added in 367328b is unsatisfied under test — fixed in the sibling PR for #1169/#1170),services/certificate-service.test.ts, andservices/serviceAccount.test.ts(which needsgoerli.infura.io). Worth noting for the CI test step: it runstimeout 30s pnpm vitest run --coverageand exits 0 when the 30 seconds elapse, so a slow or hanging suite reports success — on this machinevitest runonmaindid not finish at all, one worker sitting at 99% CPU for over six minutes.