feat(server): improve calendar sync queue (#14783)

#### PR Dependency Tree


* **PR #14783** 👈

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

* **New Features**
  * Configurable request timeout for calendar integrations.
* Calendar polling now enqueues per-subscription sync jobs (larger
batch) for improved throughput.

* **Bug Fixes / Improvements**
* Persisted next-sync timestamps and retry counts for more reliable
scheduling and retry behavior.
* Exponential backoff and webhook renewal now update scheduling
consistently.

* **Refactor**
* Calendar sync flow moved to a job-queue-driven design for better
concurrency and observability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-04-05 10:52:18 +08:00
committed by GitHub
parent bfcf7fc2ba
commit d975bf46fb
17 changed files with 352 additions and 307 deletions
@@ -92,7 +92,7 @@ export class CalendarAccountModel extends BaseModel {
scope: input.scope ?? null,
status: input.status ?? 'active',
lastError: input.lastError ?? null,
refreshIntervalMinutes: input.refreshIntervalMinutes ?? 60,
refreshIntervalMinutes: input.refreshIntervalMinutes ?? 30,
};
const updateData: Prisma.CalendarAccountUncheckedUpdateInput = {
@@ -17,6 +17,8 @@ export interface UpsertCalendarSubscriptionInput {
export interface UpdateCalendarSubscriptionSyncInput {
syncToken?: string | null;
lastSyncAt?: Date | null;
nextSyncAt?: Date;
syncRetryCount?: number;
}
export interface UpdateCalendarSubscriptionChannelInput {
@@ -81,13 +83,21 @@ export class CalendarSubscriptionModel extends BaseModel {
}
async updateSync(id: string, input: UpdateCalendarSubscriptionSyncInput) {
return await this.db.calendarSubscription.update({
where: { id },
data: {
syncToken: input.syncToken ?? null,
lastSyncAt: input.lastSyncAt ?? null,
},
});
const data: Prisma.CalendarSubscriptionUncheckedUpdateInput = {};
if (input.syncToken !== undefined) {
data.syncToken = input.syncToken ?? null;
}
if (input.lastSyncAt !== undefined) {
data.lastSyncAt = input.lastSyncAt ?? null;
}
if (input.nextSyncAt !== undefined) {
data.nextSyncAt = input.nextSyncAt;
}
if (input.syncRetryCount !== undefined) {
data.syncRetryCount = input.syncRetryCount;
}
return await this.db.calendarSubscription.update({ where: { id }, data });
}
async updateChannel(
@@ -155,10 +165,16 @@ export class CalendarSubscriptionModel extends BaseModel {
});
}
async listAllWithAccountForSync() {
async listDueForSync(now: Date, limit: number) {
return await this.db.calendarSubscription.findMany({
where: { enabled: true },
include: { account: true },
where: {
enabled: true,
nextSyncAt: { lte: now },
account: { status: 'active' },
},
select: { id: true },
orderBy: { nextSyncAt: 'asc' },
take: limit,
});
}
@@ -169,13 +185,6 @@ export class CalendarSubscriptionModel extends BaseModel {
});
}
async updateLastSyncAt(id: string, lastSyncAt: Date) {
return await this.db.calendarSubscription.update({
where: { id },
data: { lastSyncAt },
});
}
async clearSyncTokensByAccount(accountId: string) {
return await this.db.calendarSubscription.updateMany({
where: { accountId },
@@ -200,6 +209,7 @@ export class CalendarSubscriptionModel extends BaseModel {
data: {
enabled: false,
syncToken: null,
syncRetryCount: 0,
customChannelId: null,
customResourceId: null,
channelExpiration: null,