fix(meetings): evict detail cache on cancelOccurrence + map 400 error - #2210
fix(meetings): evict detail cache on cancelOccurrence + map 400 error#2210andrest50 wants to merge 2 commits into
Conversation
- Add tap(() => this.meetingDetailCache.delete(meetingId)) to the cancelOccurrence success path, consistent with updateMeeting and deleteMeeting. Without this the occurrence remained visible in the UI after a successful cancel, inviting a duplicate-cancel retry. - Add a 400 branch in CancelOccurrenceConfirmationComponent that surfaces 'This occurrence has already been cancelled — please refresh the page.' instead of the generic 'Failed to cancel' message, which was misleading when itx-service-zoom correctly rejected a concurrent second cancel request. Closes linuxfoundation/lfx-self-serve-ops#13 (issue 1 of 2) Signed-off-by: Andres Tobon <andrest2455@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Essentials Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
Pull request overview
Fixes stale meeting details after occurrence cancellation and improves concurrent-cancellation feedback.
Changes:
- Evicts cached meeting details after successful cancellation.
- Adds specialized handling for already-cancelled responses.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
meeting.service.ts |
Invalidates the meeting detail cache after cancellation. |
cancel-occurrence-confirmation.component.ts |
Maps HTTP 400 responses to cancellation feedback. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
error.status === 400 alone would match any BFF 400, including
'non-recurring meetings do not have occurrences'. The self-serve
BFF serialises the upstream error message into error.error.error
(BaseApiError.toResponse sets error: this.message). Guard with
(error.error?.error ?? '').includes('already cancelled') so only
the concurrent-cancel race case shows the friendly toast; other
400s continue to fall through to the generic error message.
Generated with [Claude Code](https://claude.ai/code)
Signed-off-by: Andres Tobon <andrest2455@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Hey @andrest50, thanks for the fast turnaround on this one. Overall impression: Small, targeted fix that closes a real gap —
Bot reconciliation: Copilot's only comment (400 too broad, would mislabel other validation failures as "already cancelled") was fixed in the very next commit ( Final decision: ✅ Approved with minor comments |
dealako
left a comment
There was a problem hiding this comment.
Automated review — see summary comment for full context. Inline notes below.
| return this.http.delete<void>(`/api/meetings/${meetingId}/occurrences/${occurrenceId}`).pipe(take(1)); | ||
| return this.http.delete<void>(`/api/meetings/${meetingId}/occurrences/${occurrenceId}`).pipe( | ||
| take(1), | ||
| tap(() => this.meetingDetailCache.delete(meetingId)) |
There was a problem hiding this comment.
[minor] Missing test coverage for cache eviction
Issue: The new tap(() => this.meetingDetailCache.delete(meetingId)) has no test verifying it fires on success and uses the correct cache key (meetingId, not occurrenceId).
Proof: No spec file exists for this frontend meeting.service.ts (the only meeting.service.spec.ts in the repo is the backend one). updateMeeting/deleteMeeting have the same gap, so this isn't a regression, but it's new logic worth covering given it's fixing a real stale-cache bug.
Why it matters: A future refactor of this pipe chain could silently drop the eviction with nothing to catch it.
Fix: Add a unit test asserting meetingDetailCache.delete is called with meetingId on success and not called on error.
| return this.http.delete<void>(`/api/meetings/${meetingId}/occurrences/${occurrenceId}`).pipe(take(1)); | ||
| return this.http.delete<void>(`/api/meetings/${meetingId}/occurrences/${occurrenceId}`).pipe( | ||
| take(1), | ||
| tap(() => this.meetingDetailCache.delete(meetingId)) |
There was a problem hiding this comment.
[nit] Inconsistent with sibling methods: no catchError logging
Issue: updateMeeting and deleteMeeting both log via catchError before rethrowing; cancelOccurrence omits it.
Proof: Compare lines ~308-336 (updateMeeting/deleteMeeting) to this method — both siblings have catchError((error) => { console.error(...); return throwError(() => error); }), this one doesn't.
Why it matters: Purely an observability gap (errors still propagate correctly to the component without it), but it breaks the established logging pattern for this class of mutation.
Fix: Add the same catchError block for consistency, e.g. logging the meetingId/occurrenceId before rethrowing.
| let errorMessage = 'Failed to cancel occurrence. Please try again.'; | ||
|
|
||
| if (error.status === 404) { | ||
| if (error.status === 400 && (error.error?.error ?? '').includes('already cancelled')) { |
There was a problem hiding this comment.
[minor] Brittle upstream-message substring match, no test
Issue: The friendly "already cancelled" message only fires if the BFF-serialized upstream error text contains the exact case-sensitive substring 'already cancelled'.
Proof: Line 43 — (error.error?.error ?? '').includes('already cancelled'). No spec file exists for this component, so there's nothing to catch a wording drift from the upstream service. This is a fail-safe brittleness (a mismatch just falls through to the generic message, not a broken error path), but it already needed one narrowing pass in this same PR (see the follow-up commit fixing the original overly-broad 400 check), which suggests the upstream contract here is worth locking down harder.
Why it matters: If itx-service-zoom ever rewords this message, the UX regresses silently back to the generic "Failed to cancel" toast with no test failure to flag it.
Fix: Add a component spec covering both the match and non-match 400 cases, and/or ask upstream for a stable error code instead of matching on free text.
| let errorMessage = 'Failed to cancel occurrence. Please try again.'; | ||
|
|
||
| if (error.status === 404) { | ||
| if (error.status === 400 && (error.error?.error ?? '').includes('already cancelled')) { |
There was a problem hiding this comment.
[nit] Hardcoded error substring vs. shared constant
Issue: 'already cancelled' is inlined here rather than defined as a shared constant.
Proof: CLAUDE.md: "All shared constants and interfaces live in @lfx-one/shared — no module-level consts." This string represents a stable upstream API contract value, similar in spirit to the constants that convention is meant to centralize.
Why it matters: low impact today, but if this string needs to be referenced from another spot later it'll drift.
Fix: Optional — could extract to packages/shared/src/constants if this pattern is reused elsewhere; not required for a single call site.
| let errorMessage = 'Failed to cancel occurrence. Please try again.'; | ||
|
|
||
| if (error.status === 404) { | ||
| if (error.status === 400 && (error.error?.error ?? '').includes('already cancelled')) { |
There was a problem hiding this comment.
[nit] Missing WHY comment for the special-case match
Issue: No comment explains why a 400 needs this specific string match rather than being treated as a generic client error.
Proof: Line 43 — the reasoning (itx-service-zoom returns a plain 400 rather than a distinct status/code for a concurrent-cancel race) is only in the commit message, not the code.
Why it matters: The repo convention is to comment non-obvious WHY logic; a future reader has no clue why this particular substring match exists without digging into git blame.
Fix: One-line comment above the branch, e.g. noting itx-service-zoom returns a plain 400 (not a distinct code) when the occurrence is already cancelled.
Summary
Fixes issue 1 of 2 from linuxfoundation/lfx-self-serve-ops#13: the "occurrence already cancelled" UX problem caused by a concurrent-session race condition amplified by stale caching.
Root cause
Two independent caching gaps meant a successful
cancelOccurrenceleft the occurrence still visible in the UI:MeetingService.cancelOccurrencewas not evicting themeetingDetailCacheentry on success, unlikeupdateMeetinganddeleteMeetingwhich both callthis.meetingDetailCache.delete(id). The occurrence remained visible in the same session until the TTL expired.400 "occurrence for meeting is already cancelled"fromitx-service-zoom. The component mapped this to the generic "Failed to cancel occurrence. Please try again." — which encouraged further retries.Changes
apps/lfx-one/src/app/shared/services/meeting.service.tstap(() => this.meetingDetailCache.delete(meetingId))to thecancelOccurrencepipe on success, consistent withupdateMeeting(line 315) anddeleteMeeting(line 330).apps/lfx-one/src/app/modules/meetings/components/cancel-occurrence-confirmation/cancel-occurrence-confirmation.component.ts400branch that maps to: "This occurrence has already been cancelled — please refresh the page." instead of the generic retry prompt.Testing
Made with Cursor