feat: handle calendar sync failed (#14510)

#### PR Dependency Tree


* **PR #14510** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved calendar sync reliability with exponential backoff for
repeated failures.
* Better handling of token refresh failures with automatic account
invalidation and cleanup when needed.
* Subscriptions are now automatically disabled and related events
removed when the calendar provider reports missing resources.

* **Tests**
* Added comprehensive tests covering sync failures, backoff behavior,
token refresh handling, skipping retries during backoff, and recovery.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-24 22:45:47 +08:00
committed by GitHub
parent 9c99293c92
commit 41b3b0e82e
4 changed files with 488 additions and 35 deletions
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import type { CalendarAccount, Prisma } from '@prisma/client';
import { CryptoHelper } from '../base';
@@ -174,6 +175,18 @@ export class CalendarAccountModel extends BaseModel {
});
}
@Transactional()
async invalidateAndPurge(id: string, lastError?: string | null) {
await this.updateStatus(id, 'invalid', lastError ?? null);
const subscriptions =
await this.models.calendarSubscription.listByAccount(id);
const subscriptionIds = subscriptions.map(subscription => subscription.id);
if (subscriptionIds.length > 0) {
await this.models.calendarEvent.deleteBySubscriptionIds(subscriptionIds);
}
await this.models.calendarSubscription.clearSyncTokensByAccount(id);
}
async delete(id: string) {
return await this.db.calendarAccount.delete({
where: { id },
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import type { CalendarSubscription, Prisma } from '@prisma/client';
import { BaseModel } from './base';
@@ -191,4 +192,20 @@ export class CalendarSubscriptionModel extends BaseModel {
data,
});
}
@Transactional()
async disableAndPurge(subscriptionId: string) {
await this.db.calendarSubscription.update({
where: { id: subscriptionId },
data: {
enabled: false,
syncToken: null,
customChannelId: null,
customResourceId: null,
channelExpiration: null,
},
});
await this.models.calendarEvent.deleteBySubscriptionIds([subscriptionId]);
}
}